@lifecorner/angular-ui-kit 3.27.2 → 3.28.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,10 @@
1
1
  import * as i0 from "@angular/core";
2
2
  import { ChangeDetectionStrategy, Component, DestroyRef, afterNextRender, computed, inject, input, viewChild } from "@angular/core";
3
+ import { OverlayInsetsService } from "@lifecorner/angular-ui-kit/internal/overlay";
3
4
  const BAR_VAR = "--_lc-app-shell-bar";
5
+ const TOP_INSET = "env(safe-area-inset-top, 0px)";
6
+ const BOTTOM_INSET = "env(safe-area-inset-bottom, 0px)";
7
+ const borderBoxHeight = (entry, target) => entry.borderBoxSize?.[0]?.blockSize ?? target.getBoundingClientRect().height;
4
8
  var AppShellComponent = class AppShellComponent {
5
9
  horizon = input(false, ...ngDevMode ? [{ debugName: "horizon" }] : /* istanbul ignore next */ []);
6
10
  navPlacement = input("below-header", ...ngDevMode ? [{ debugName: "navPlacement" }] : /* istanbul ignore next */ []);
@@ -11,21 +15,37 @@ var AppShellComponent = class AppShellComponent {
11
15
  return classes.join(" ");
12
16
  }, ...ngDevMode ? [{ debugName: "shellClasses" }] : /* istanbul ignore next */ []);
13
17
  shell = viewChild.required("shell", ...ngDevMode ? [{ debugName: "shell" }] : /* istanbul ignore next */ []);
18
+ headerSlot = viewChild.required("headerSlot", ...ngDevMode ? [{ debugName: "headerSlot" }] : /* istanbul ignore next */ []);
14
19
  barSlot = viewChild.required("barSlot", ...ngDevMode ? [{ debugName: "barSlot" }] : /* istanbul ignore next */ []);
15
20
  constructor() {
16
21
  const destroyRef = inject(DestroyRef);
22
+ const insets = inject(OverlayInsetsService);
17
23
  afterNextRender(() => {
18
24
  if (typeof ResizeObserver === "undefined") return;
19
25
  const shell = this.shell().nativeElement;
26
+ const header = this.headerSlot().nativeElement;
20
27
  const slot = this.barSlot().nativeElement;
21
- const clear = () => shell.style.removeProperty(BAR_VAR);
28
+ insets.top.set(TOP_INSET);
29
+ insets.bottom.set(BOTTOM_INSET);
30
+ let bar = null;
31
+ const clear = () => {
32
+ shell.style.removeProperty(BAR_VAR);
33
+ insets.bottom.set(BOTTOM_INSET);
34
+ };
22
35
  const sizes = new ResizeObserver((entries) => {
23
36
  const entry = entries[entries.length - 1];
24
37
  const height = entry.contentBoxSize?.[0]?.blockSize ?? entry.contentRect.height;
25
- if (height > 0) shell.style.setProperty(BAR_VAR, `${height}px`);
26
- else clear();
38
+ if (height > 0 && bar) {
39
+ shell.style.setProperty(BAR_VAR, `${height}px`);
40
+ const bottom = parseFloat(getComputedStyle(bar).bottom);
41
+ insets.bottom.set(`${(Number.isFinite(bottom) ? bottom : 0) + borderBoxHeight(entry, bar)}px`);
42
+ } else clear();
27
43
  });
28
- let bar = null;
44
+ const headerSizes = new ResizeObserver((entries) => {
45
+ const height = borderBoxHeight(entries[entries.length - 1], header);
46
+ insets.top.set(height > 0 ? `${height}px` : TOP_INSET);
47
+ });
48
+ headerSizes.observe(header);
29
49
  const pick = () => {
30
50
  const next = slot.querySelector(".lc-bottom-nav") ?? slot.firstElementChild;
31
51
  if (next === bar) return;
@@ -42,7 +62,10 @@ var AppShellComponent = class AppShellComponent {
42
62
  });
43
63
  destroyRef.onDestroy(() => {
44
64
  sizes.disconnect();
65
+ headerSizes.disconnect();
45
66
  changes?.disconnect();
67
+ insets.top.set(null);
68
+ insets.bottom.set(null);
46
69
  });
47
70
  });
48
71
  }
@@ -76,21 +99,31 @@ var AppShellComponent = class AppShellComponent {
76
99
  transformFunction: null
77
100
  }
78
101
  },
79
- viewQueries: [{
80
- propertyName: "shell",
81
- first: true,
82
- predicate: ["shell"],
83
- descendants: true,
84
- isSignal: true
85
- }, {
86
- propertyName: "barSlot",
87
- first: true,
88
- predicate: ["barSlot"],
89
- descendants: true,
90
- isSignal: true
91
- }],
102
+ viewQueries: [
103
+ {
104
+ propertyName: "shell",
105
+ first: true,
106
+ predicate: ["shell"],
107
+ descendants: true,
108
+ isSignal: true
109
+ },
110
+ {
111
+ propertyName: "headerSlot",
112
+ first: true,
113
+ predicate: ["headerSlot"],
114
+ descendants: true,
115
+ isSignal: true
116
+ },
117
+ {
118
+ propertyName: "barSlot",
119
+ first: true,
120
+ predicate: ["barSlot"],
121
+ descendants: true,
122
+ isSignal: true
123
+ }
124
+ ],
92
125
  ngImport: i0,
93
- template: "<div #shell [class]=\"shellClasses()\">\n <div class=\"lc-app-shell__header\"><ng-content select=\"[shell-header]\" /></div>\n <div class=\"lc-app-shell__nav\"><ng-content select=\"[shell-nav]\" /></div>\n <div class=\"lc-app-shell__body\"><ng-content /></div>\n @if (horizon()) {\n <div class=\"lc-app-shell__horizon lc-horizon\" aria-hidden=\"true\"><ng-content select=\"[shell-horizon-figure]\" /></div>\n }\n <div class=\"lc-app-shell__footer\"><ng-content select=\"[shell-footer]\" /></div>\n <div #barSlot class=\"lc-app-shell__bottom-nav\"><ng-content select=\"[shell-bottom-nav]\" /></div>\n</div>\n",
126
+ template: "<div #shell [class]=\"shellClasses()\">\n <div #headerSlot class=\"lc-app-shell__header\"><ng-content select=\"[shell-header]\" /></div>\n <div class=\"lc-app-shell__nav\"><ng-content select=\"[shell-nav]\" /></div>\n <div class=\"lc-app-shell__body\"><ng-content /></div>\n @if (horizon()) {\n <div class=\"lc-app-shell__horizon lc-horizon\" aria-hidden=\"true\"><ng-content select=\"[shell-horizon-figure]\" /></div>\n }\n <div class=\"lc-app-shell__footer\"><ng-content select=\"[shell-footer]\" /></div>\n <div #barSlot class=\"lc-app-shell__bottom-nav\"><ng-content select=\"[shell-bottom-nav]\" /></div>\n</div>\n",
94
127
  styles: [":host{display:block}.lc-app-shell{--_nav-width: var(--lc-app-shell-nav-width, 240px);--_clearance: var( --lc-app-shell-bottom-clearance, max( calc(112px + env(safe-area-inset-bottom, 0px)) , calc(var(--_lc-app-shell-bar, 72px) + 40px + env(safe-area-inset-bottom, 0px))) );display:grid;grid-template-columns:minmax(0,1fr);grid-template-rows:auto auto 1fr;grid-template-areas:\"header\" \"body\" \"end\";min-height:100vh;min-height:100dvh;box-sizing:border-box;background-color:var(--lc-app-shell-bg, var(--color-background));background-image:var(--lc-app-shell-bg-image, var(--app-background-image, none))}.lc-app-shell__header{grid-area:header;min-width:0;padding-top:env(safe-area-inset-top,0px);padding-left:env(safe-area-inset-left,0px);padding-right:env(safe-area-inset-right,0px)}.lc-app-shell__body{grid-area:body;min-width:0;padding-left:env(safe-area-inset-left,0px);padding-right:env(safe-area-inset-right,0px)}.lc-app-shell__nav{display:none}.lc-app-shell__horizon{grid-area:end;min-width:0}.lc-app-shell__footer{grid-area:end;align-self:end;min-width:0;padding-left:env(safe-area-inset-left,0px);padding-right:env(safe-area-inset-right,0px)}.lc-app-shell--horizon{grid-template-rows:auto auto 1fr auto}.lc-app-shell--horizon .lc-app-shell__footer{position:relative;z-index:1;grid-row:4;grid-column:1/-1;background-color:var(--color-land-3)}.lc-app-shell__footer:empty,.lc-app-shell__header:empty{display:none}.lc-app-shell__bottom-nav{display:contents}@media(max-width:1023.98px){.lc-app-shell:not(.lc-app-shell--horizon):has(>.lc-app-shell__bottom-nav:not(:empty)){padding-bottom:var(--_clearance)}.lc-app-shell--horizon:has(>.lc-app-shell__bottom-nav:not(:empty))>.lc-app-shell__horizon{--_lc-horizon-land: calc(var(--lc-horizon-land, 112px) + max(0px, var(--_lc-app-shell-bar, 72px) - 72px))}}@media(min-width:1024px){.lc-app-shell{grid-template-columns:var(--_nav-width) minmax(0,1fr);grid-template-areas:\"header header\" \"nav body\" \"end end\"}.lc-app-shell:has(>.lc-app-shell__nav:empty){grid-template-columns:minmax(0,1fr);grid-template-areas:\"header\" \"body\" \"end\"}.lc-app-shell__nav{grid-area:nav;display:block;min-width:0;position:sticky;top:0;align-self:start;max-height:100vh;max-height:100dvh;overflow-y:auto;padding-left:env(safe-area-inset-left,0px)}.lc-app-shell__nav:empty{display:none}.lc-app-shell:not(:has(>.lc-app-shell__nav:empty)) .lc-app-shell__body{padding-left:0}.lc-app-shell__bottom-nav{display:none}.lc-app-shell--nav-full-height:not(:has(>.lc-app-shell__nav:empty)){grid-template-areas:\"nav header\" \"nav body\" \"end end\"}.lc-app-shell--nav-full-height:not(:has(>.lc-app-shell__nav:empty))>.lc-app-shell__header{padding-left:0}.lc-app-shell--nav-full-height:not(:has(>.lc-app-shell__nav:empty))>.lc-app-shell__nav{grid-row:1/-1;grid-column:1;box-sizing:border-box;height:100vh;height:100dvh;max-height:none;z-index:2;padding-top:env(safe-area-inset-top,0px);display:grid;grid-template-columns:minmax(0,1fr);grid-template-rows:minmax(100%,auto)}.lc-app-shell--nav-full-height:not(:has(>.lc-app-shell__nav:empty))>.lc-app-shell__footer{grid-column:1/-1;padding-left:var(--_nav-width)}}@media(min-width:1280px){.lc-app-shell:has(.lc-app-page__aside)>.lc-app-shell__header{--_aside-width: var(--lc-app-page-aside-width, 360px);--_hairline: var(--color-hairline, var(--color-border));background:linear-gradient(var(--_hairline),var(--_hairline)) right calc(var(--_aside-width) - 1px) top/1px 100% no-repeat,linear-gradient(var(--color-background-low),var(--color-background-low)) right top/var(--_aside-width) 100% no-repeat}}@media(min-width:1440px){.lc-app-shell{--_nav-width: var(--lc-app-shell-nav-width, 248px)}.lc-app-shell:has(.lc-app-page__aside)>.lc-app-shell__header{--_aside-width: var(--lc-app-page-aside-width, 400px)}}@media(min-width:1920px){.lc-app-shell{--_nav-width: var(--lc-app-shell-nav-width, 280px)}.lc-app-shell:has(.lc-app-page__aside)>.lc-app-shell__header{--_aside-width: var(--lc-app-page-aside-width, 460px)}}@media(forced-colors:active){.lc-app-shell__header{background:none}}\n"],
95
128
  changeDetection: i0.ChangeDetectionStrategy.OnPush
96
129
  });
@@ -106,7 +139,7 @@ i0.ɵɵngDeclareClassMetadata({
106
139
  selector: "lc-app-shell",
107
140
  standalone: true,
108
141
  changeDetection: ChangeDetectionStrategy.OnPush,
109
- template: "<div #shell [class]=\"shellClasses()\">\n <div class=\"lc-app-shell__header\"><ng-content select=\"[shell-header]\" /></div>\n <div class=\"lc-app-shell__nav\"><ng-content select=\"[shell-nav]\" /></div>\n <div class=\"lc-app-shell__body\"><ng-content /></div>\n @if (horizon()) {\n <div class=\"lc-app-shell__horizon lc-horizon\" aria-hidden=\"true\"><ng-content select=\"[shell-horizon-figure]\" /></div>\n }\n <div class=\"lc-app-shell__footer\"><ng-content select=\"[shell-footer]\" /></div>\n <div #barSlot class=\"lc-app-shell__bottom-nav\"><ng-content select=\"[shell-bottom-nav]\" /></div>\n</div>\n",
142
+ template: "<div #shell [class]=\"shellClasses()\">\n <div #headerSlot class=\"lc-app-shell__header\"><ng-content select=\"[shell-header]\" /></div>\n <div class=\"lc-app-shell__nav\"><ng-content select=\"[shell-nav]\" /></div>\n <div class=\"lc-app-shell__body\"><ng-content /></div>\n @if (horizon()) {\n <div class=\"lc-app-shell__horizon lc-horizon\" aria-hidden=\"true\"><ng-content select=\"[shell-horizon-figure]\" /></div>\n }\n <div class=\"lc-app-shell__footer\"><ng-content select=\"[shell-footer]\" /></div>\n <div #barSlot class=\"lc-app-shell__bottom-nav\"><ng-content select=\"[shell-bottom-nav]\" /></div>\n</div>\n",
110
143
  styles: [":host{display:block}.lc-app-shell{--_nav-width: var(--lc-app-shell-nav-width, 240px);--_clearance: var( --lc-app-shell-bottom-clearance, max( calc(112px + env(safe-area-inset-bottom, 0px)) , calc(var(--_lc-app-shell-bar, 72px) + 40px + env(safe-area-inset-bottom, 0px))) );display:grid;grid-template-columns:minmax(0,1fr);grid-template-rows:auto auto 1fr;grid-template-areas:\"header\" \"body\" \"end\";min-height:100vh;min-height:100dvh;box-sizing:border-box;background-color:var(--lc-app-shell-bg, var(--color-background));background-image:var(--lc-app-shell-bg-image, var(--app-background-image, none))}.lc-app-shell__header{grid-area:header;min-width:0;padding-top:env(safe-area-inset-top,0px);padding-left:env(safe-area-inset-left,0px);padding-right:env(safe-area-inset-right,0px)}.lc-app-shell__body{grid-area:body;min-width:0;padding-left:env(safe-area-inset-left,0px);padding-right:env(safe-area-inset-right,0px)}.lc-app-shell__nav{display:none}.lc-app-shell__horizon{grid-area:end;min-width:0}.lc-app-shell__footer{grid-area:end;align-self:end;min-width:0;padding-left:env(safe-area-inset-left,0px);padding-right:env(safe-area-inset-right,0px)}.lc-app-shell--horizon{grid-template-rows:auto auto 1fr auto}.lc-app-shell--horizon .lc-app-shell__footer{position:relative;z-index:1;grid-row:4;grid-column:1/-1;background-color:var(--color-land-3)}.lc-app-shell__footer:empty,.lc-app-shell__header:empty{display:none}.lc-app-shell__bottom-nav{display:contents}@media(max-width:1023.98px){.lc-app-shell:not(.lc-app-shell--horizon):has(>.lc-app-shell__bottom-nav:not(:empty)){padding-bottom:var(--_clearance)}.lc-app-shell--horizon:has(>.lc-app-shell__bottom-nav:not(:empty))>.lc-app-shell__horizon{--_lc-horizon-land: calc(var(--lc-horizon-land, 112px) + max(0px, var(--_lc-app-shell-bar, 72px) - 72px))}}@media(min-width:1024px){.lc-app-shell{grid-template-columns:var(--_nav-width) minmax(0,1fr);grid-template-areas:\"header header\" \"nav body\" \"end end\"}.lc-app-shell:has(>.lc-app-shell__nav:empty){grid-template-columns:minmax(0,1fr);grid-template-areas:\"header\" \"body\" \"end\"}.lc-app-shell__nav{grid-area:nav;display:block;min-width:0;position:sticky;top:0;align-self:start;max-height:100vh;max-height:100dvh;overflow-y:auto;padding-left:env(safe-area-inset-left,0px)}.lc-app-shell__nav:empty{display:none}.lc-app-shell:not(:has(>.lc-app-shell__nav:empty)) .lc-app-shell__body{padding-left:0}.lc-app-shell__bottom-nav{display:none}.lc-app-shell--nav-full-height:not(:has(>.lc-app-shell__nav:empty)){grid-template-areas:\"nav header\" \"nav body\" \"end end\"}.lc-app-shell--nav-full-height:not(:has(>.lc-app-shell__nav:empty))>.lc-app-shell__header{padding-left:0}.lc-app-shell--nav-full-height:not(:has(>.lc-app-shell__nav:empty))>.lc-app-shell__nav{grid-row:1/-1;grid-column:1;box-sizing:border-box;height:100vh;height:100dvh;max-height:none;z-index:2;padding-top:env(safe-area-inset-top,0px);display:grid;grid-template-columns:minmax(0,1fr);grid-template-rows:minmax(100%,auto)}.lc-app-shell--nav-full-height:not(:has(>.lc-app-shell__nav:empty))>.lc-app-shell__footer{grid-column:1/-1;padding-left:var(--_nav-width)}}@media(min-width:1280px){.lc-app-shell:has(.lc-app-page__aside)>.lc-app-shell__header{--_aside-width: var(--lc-app-page-aside-width, 360px);--_hairline: var(--color-hairline, var(--color-border));background:linear-gradient(var(--_hairline),var(--_hairline)) right calc(var(--_aside-width) - 1px) top/1px 100% no-repeat,linear-gradient(var(--color-background-low),var(--color-background-low)) right top/var(--_aside-width) 100% no-repeat}}@media(min-width:1440px){.lc-app-shell{--_nav-width: var(--lc-app-shell-nav-width, 248px)}.lc-app-shell:has(.lc-app-page__aside)>.lc-app-shell__header{--_aside-width: var(--lc-app-page-aside-width, 400px)}}@media(min-width:1920px){.lc-app-shell{--_nav-width: var(--lc-app-shell-nav-width, 280px)}.lc-app-shell:has(.lc-app-page__aside)>.lc-app-shell__header{--_aside-width: var(--lc-app-page-aside-width, 460px)}}@media(forced-colors:active){.lc-app-shell__header{background:none}}\n"]
111
144
  }]
112
145
  }],
@@ -132,6 +165,10 @@ i0.ɵɵngDeclareClassMetadata({
132
165
  type: i0.ViewChild,
133
166
  args: ["shell", { isSignal: true }]
134
167
  }],
168
+ headerSlot: [{
169
+ type: i0.ViewChild,
170
+ args: ["headerSlot", { isSignal: true }]
171
+ }],
135
172
  barSlot: [{
136
173
  type: i0.ViewChild,
137
174
  args: ["barSlot", { isSignal: true }]
@@ -146,10 +183,21 @@ var AppPageComponent = class AppPageComponent {
146
183
  busy = input(false, ...ngDevMode ? [{ debugName: "busy" }] : /* istanbul ignore next */ []);
147
184
  paddingX = input("default", ...ngDevMode ? [{ debugName: "paddingX" }] : /* istanbul ignore next */ []);
148
185
  paddingTop = input("default", ...ngDevMode ? [{ debugName: "paddingTop" }] : /* istanbul ignore next */ []);
186
+ reserveAside = input(false, ...ngDevMode ? [{ debugName: "reserveAside" }] : /* istanbul ignore next */ []);
187
+ contentWidth = input("default", ...ngDevMode ? [{ debugName: "contentWidth" }] : /* istanbul ignore next */ []);
149
188
  hasAside = computed(() => !!(this.asideLabelledby() || this.asideAriaLabel()), ...ngDevMode ? [{ debugName: "hasAside" }] : /* istanbul ignore next */ []);
189
+ wide = computed(() => this.contentWidth() === "wide" && !this.hasAside(), ...ngDevMode ? [{ debugName: "wide" }] : /* istanbul ignore next */ []);
190
+ reserved = computed(() => this.reserveAside() && !this.hasAside() && !this.wide(), ...ngDevMode ? [{ debugName: "reserved" }] : /* istanbul ignore next */ []);
191
+ wideMax = computed(() => {
192
+ if (!this.wide()) return null;
193
+ const max = this.contentMaxWidth();
194
+ if (max === "880px") return null;
195
+ return max === "none" ? "100vw" : max;
196
+ }, ...ngDevMode ? [{ debugName: "wideMax" }] : /* istanbul ignore next */ []);
150
197
  pageClasses = computed(() => {
151
198
  const classes = ["lc-app-page"];
152
- if (this.hasAside()) classes.push("lc-app-page--aside");
199
+ if (this.hasAside() || this.reserveAside() || this.wide()) classes.push("lc-app-page--aside");
200
+ if (this.wide()) classes.push("lc-app-page--wide");
153
201
  if (this.paddingX() === "header") classes.push("lc-app-page--pad-x-header");
154
202
  if (this.paddingTop() !== "default") classes.push(`lc-app-page--pad-top-${this.paddingTop()}`);
155
203
  return classes.join(" ");
@@ -217,11 +265,25 @@ var AppPageComponent = class AppPageComponent {
217
265
  isSignal: true,
218
266
  isRequired: false,
219
267
  transformFunction: null
268
+ },
269
+ reserveAside: {
270
+ classPropertyName: "reserveAside",
271
+ publicName: "reserveAside",
272
+ isSignal: true,
273
+ isRequired: false,
274
+ transformFunction: null
275
+ },
276
+ contentWidth: {
277
+ classPropertyName: "contentWidth",
278
+ publicName: "contentWidth",
279
+ isSignal: true,
280
+ isRequired: false,
281
+ transformFunction: null
220
282
  }
221
283
  },
222
284
  ngImport: i0,
223
- template: "<div [class]=\"pageClasses()\" [style.--lc-app-page-content-max]=\"contentMaxWidth()\">\n <main class=\"lc-app-page__main\" [id]=\"mainId()\" [attr.aria-busy]=\"busy() ? 'true' : null\">\n <div class=\"lc-app-page__content\"><ng-content /></div>\n </main>\n @if (hasAside()) {\n <aside\n class=\"lc-app-page__aside\"\n [attr.aria-labelledby]=\"asideLabelledby() || null\"\n [attr.aria-label]=\"asideLabelledby() ? null : asideAriaLabel()\"\n >\n <ng-content select=\"[page-aside]\" />\n </aside>\n }\n</div>\n",
224
- styles: [":host{display:block;min-width:0}.lc-app-page{--_pad-x: var(--lc-app-page-padding-x, 24px);--_pad-top: var(--lc-app-page-padding-top, 24px);--_gap: var(--lc-app-page-gap, 24px);--_aside-width: var(--lc-app-page-aside-width, 360px);display:grid;grid-template-columns:minmax(0,1fr);min-height:100%}.lc-app-page__main{display:block;min-width:0;padding:var(--_pad-top) var(--_pad-x) 16px}.lc-app-page__content{display:flex;flex-direction:column;gap:var(--_gap);max-width:var(--lc-app-page-content-max, 880px);margin-inline:auto}.lc-app-page__aside{display:none}@media(min-width:640px){.lc-app-page{--_pad-x: var(--lc-app-page-padding-x, 32px);--_pad-top: var(--lc-app-page-padding-top, 32px)}}@media(min-width:1024px){.lc-app-page{--_pad-x: var(--lc-app-page-padding-x, 48px);--_pad-top: var(--lc-app-page-padding-top, 48px);--_gap: var(--lc-app-page-gap, 40px)}.lc-app-page__main{padding-bottom:32px}}@media(min-width:1280px){.lc-app-page{--_pad-x: var(--lc-app-page-padding-x, 56px);--_pad-top: var(--lc-app-page-padding-top, 56px)}.lc-app-page--aside{grid-template-columns:minmax(0,1fr) var(--_aside-width)}.lc-app-page__aside{display:block;min-width:0;box-sizing:border-box;padding:max(16px,var(--_pad-top) - 32px) 40px 40px;background:linear-gradient(180deg,var(--color-background-low),transparent);border-left:1px solid var(--color-hairline, var(--color-border));border-image:linear-gradient(180deg,var(--color-hairline, var(--color-border)) 60%,transparent) 1}}@media(min-width:1440px){.lc-app-page{--_pad-x: var(--lc-app-page-padding-x, 72px);--_pad-top: var(--lc-app-page-padding-top, 72px);--_aside-width: var(--lc-app-page-aside-width, 400px)}}@media(min-width:1920px){.lc-app-page{--_pad-x: var(--lc-app-page-padding-x, 96px);--_pad-top: var(--lc-app-page-padding-top, 112px);--_aside-width: var(--lc-app-page-aside-width, 460px)}}.lc-app-page.lc-app-page--pad-x-header{--_pad-x: var(--lc-app-page-padding-x, var(--lc-density-padding-lg, var(--spacing-6)))}@media(max-width:768px){.lc-app-page.lc-app-page--pad-x-header{--_pad-x: var(--lc-app-page-padding-x, var(--lc-density-padding-md, var(--spacing-4)))}}.lc-app-page.lc-app-page--pad-top-none{--_pad-top: var(--lc-app-page-padding-top, 0px)}.lc-app-page.lc-app-page--pad-top-sm{--_pad-top: var(--lc-app-page-padding-top, var(--spacing-2))}.lc-app-page.lc-app-page--pad-top-md{--_pad-top: var(--lc-app-page-padding-top, var(--spacing-4))}@media(forced-colors:active){.lc-app-page__aside{background:none;border-image:none;border-left-color:CanvasText}}\n"],
285
+ template: "<div [class]=\"pageClasses()\" [style.--lc-app-page-content-max]=\"contentMaxWidth()\" [style.--_lc-app-page-wide-max]=\"wideMax()\">\n <main class=\"lc-app-page__main\" [id]=\"mainId()\" [attr.aria-busy]=\"busy() ? 'true' : null\">\n <div class=\"lc-app-page__content\"><ng-content /></div>\n </main>\n <!-- One outer block, so a page without a right column keeps 3.27.2's single\n anchor (an @else branch adds one); the reserved column (AS-35) is empty,\n aria-hidden and no landmark. -->\n @if (hasAside() || reserved()) {\n @if (hasAside()) {\n <aside\n class=\"lc-app-page__aside\"\n [attr.aria-labelledby]=\"asideLabelledby() || null\"\n [attr.aria-label]=\"asideLabelledby() ? null : asideAriaLabel()\"\n >\n <ng-content select=\"[page-aside]\" />\n </aside>\n } @else {<div class=\"lc-app-page__aside lc-app-page__aside--reserved\" aria-hidden=\"true\"></div>}\n }\n</div>\n",
286
+ styles: [":host{display:block;min-width:0}.lc-app-page{--_pad-x: var(--lc-app-page-padding-x, 24px);--_pad-top: var(--lc-app-page-padding-top, 24px);--_gap: var(--lc-app-page-gap, 24px);--_aside-width: var(--lc-app-page-aside-width, 360px);display:grid;grid-template-columns:minmax(0,1fr);min-height:100%}.lc-app-page__main{display:block;min-width:0;padding:var(--_pad-top) var(--_pad-x) 16px}.lc-app-page__content{display:flex;flex-direction:column;gap:var(--_gap);max-width:var(--lc-app-page-content-max, 880px);margin-inline:auto}.lc-app-page__aside{display:none}@media(min-width:640px){.lc-app-page{--_pad-x: var(--lc-app-page-padding-x, 32px);--_pad-top: var(--lc-app-page-padding-top, 32px)}}@media(min-width:1024px){.lc-app-page{--_pad-x: var(--lc-app-page-padding-x, 48px);--_pad-top: var(--lc-app-page-padding-top, 48px);--_gap: var(--lc-app-page-gap, 40px)}.lc-app-page__main{padding-bottom:32px}}@media(min-width:1280px){.lc-app-page{--_pad-x: var(--lc-app-page-padding-x, 56px);--_pad-top: var(--lc-app-page-padding-top, 56px)}.lc-app-page--aside{grid-template-columns:minmax(0,1fr) var(--_aside-width)}.lc-app-page__aside{display:block;min-width:0;box-sizing:border-box;padding:max(16px,var(--_pad-top) - 32px) 40px 40px;background:linear-gradient(180deg,var(--color-background-low),transparent);border-left:1px solid var(--color-hairline, var(--color-border));border-image:linear-gradient(180deg,var(--color-hairline, var(--color-border)) 60%,transparent) 1}}@media(min-width:1440px){.lc-app-page{--_pad-x: var(--lc-app-page-padding-x, 72px);--_pad-top: var(--lc-app-page-padding-top, 72px);--_aside-width: var(--lc-app-page-aside-width, 400px)}}@media(min-width:1920px){.lc-app-page{--_pad-x: var(--lc-app-page-padding-x, 96px);--_pad-top: var(--lc-app-page-padding-top, 112px);--_aside-width: var(--lc-app-page-aside-width, 460px)}}.lc-app-page.lc-app-page--pad-x-header{--_pad-x: var(--lc-app-page-padding-x, var(--lc-density-padding-lg, var(--spacing-6)))}@media(max-width:768px){.lc-app-page.lc-app-page--pad-x-header{--_pad-x: var(--lc-app-page-padding-x, var(--lc-density-padding-md, var(--spacing-4)))}}.lc-app-page.lc-app-page--pad-top-none{--_pad-top: var(--lc-app-page-padding-top, 0px)}.lc-app-page.lc-app-page--pad-top-sm{--_pad-top: var(--lc-app-page-padding-top, var(--spacing-2))}.lc-app-page.lc-app-page--pad-top-md{--_pad-top: var(--lc-app-page-padding-top, var(--spacing-4))}@media(forced-colors:active){.lc-app-page__aside{background:none;border-image:none;border-left-color:CanvasText}}@media(min-width:1280px){:host ::ng-deep .lc-app-page--wide>.lc-app-page__main>.lc-app-page__content>[page-wide]{box-sizing:border-box;width:min(var(--_lc-app-page-wide-max, 928px),100% + var(--_aside-width));max-width:none}}\n"],
225
287
  changeDetection: i0.ChangeDetectionStrategy.OnPush
226
288
  });
227
289
  };
@@ -236,8 +298,8 @@ i0.ɵɵngDeclareClassMetadata({
236
298
  selector: "lc-app-page",
237
299
  standalone: true,
238
300
  changeDetection: ChangeDetectionStrategy.OnPush,
239
- template: "<div [class]=\"pageClasses()\" [style.--lc-app-page-content-max]=\"contentMaxWidth()\">\n <main class=\"lc-app-page__main\" [id]=\"mainId()\" [attr.aria-busy]=\"busy() ? 'true' : null\">\n <div class=\"lc-app-page__content\"><ng-content /></div>\n </main>\n @if (hasAside()) {\n <aside\n class=\"lc-app-page__aside\"\n [attr.aria-labelledby]=\"asideLabelledby() || null\"\n [attr.aria-label]=\"asideLabelledby() ? null : asideAriaLabel()\"\n >\n <ng-content select=\"[page-aside]\" />\n </aside>\n }\n</div>\n",
240
- styles: [":host{display:block;min-width:0}.lc-app-page{--_pad-x: var(--lc-app-page-padding-x, 24px);--_pad-top: var(--lc-app-page-padding-top, 24px);--_gap: var(--lc-app-page-gap, 24px);--_aside-width: var(--lc-app-page-aside-width, 360px);display:grid;grid-template-columns:minmax(0,1fr);min-height:100%}.lc-app-page__main{display:block;min-width:0;padding:var(--_pad-top) var(--_pad-x) 16px}.lc-app-page__content{display:flex;flex-direction:column;gap:var(--_gap);max-width:var(--lc-app-page-content-max, 880px);margin-inline:auto}.lc-app-page__aside{display:none}@media(min-width:640px){.lc-app-page{--_pad-x: var(--lc-app-page-padding-x, 32px);--_pad-top: var(--lc-app-page-padding-top, 32px)}}@media(min-width:1024px){.lc-app-page{--_pad-x: var(--lc-app-page-padding-x, 48px);--_pad-top: var(--lc-app-page-padding-top, 48px);--_gap: var(--lc-app-page-gap, 40px)}.lc-app-page__main{padding-bottom:32px}}@media(min-width:1280px){.lc-app-page{--_pad-x: var(--lc-app-page-padding-x, 56px);--_pad-top: var(--lc-app-page-padding-top, 56px)}.lc-app-page--aside{grid-template-columns:minmax(0,1fr) var(--_aside-width)}.lc-app-page__aside{display:block;min-width:0;box-sizing:border-box;padding:max(16px,var(--_pad-top) - 32px) 40px 40px;background:linear-gradient(180deg,var(--color-background-low),transparent);border-left:1px solid var(--color-hairline, var(--color-border));border-image:linear-gradient(180deg,var(--color-hairline, var(--color-border)) 60%,transparent) 1}}@media(min-width:1440px){.lc-app-page{--_pad-x: var(--lc-app-page-padding-x, 72px);--_pad-top: var(--lc-app-page-padding-top, 72px);--_aside-width: var(--lc-app-page-aside-width, 400px)}}@media(min-width:1920px){.lc-app-page{--_pad-x: var(--lc-app-page-padding-x, 96px);--_pad-top: var(--lc-app-page-padding-top, 112px);--_aside-width: var(--lc-app-page-aside-width, 460px)}}.lc-app-page.lc-app-page--pad-x-header{--_pad-x: var(--lc-app-page-padding-x, var(--lc-density-padding-lg, var(--spacing-6)))}@media(max-width:768px){.lc-app-page.lc-app-page--pad-x-header{--_pad-x: var(--lc-app-page-padding-x, var(--lc-density-padding-md, var(--spacing-4)))}}.lc-app-page.lc-app-page--pad-top-none{--_pad-top: var(--lc-app-page-padding-top, 0px)}.lc-app-page.lc-app-page--pad-top-sm{--_pad-top: var(--lc-app-page-padding-top, var(--spacing-2))}.lc-app-page.lc-app-page--pad-top-md{--_pad-top: var(--lc-app-page-padding-top, var(--spacing-4))}@media(forced-colors:active){.lc-app-page__aside{background:none;border-image:none;border-left-color:CanvasText}}\n"]
301
+ template: "<div [class]=\"pageClasses()\" [style.--lc-app-page-content-max]=\"contentMaxWidth()\" [style.--_lc-app-page-wide-max]=\"wideMax()\">\n <main class=\"lc-app-page__main\" [id]=\"mainId()\" [attr.aria-busy]=\"busy() ? 'true' : null\">\n <div class=\"lc-app-page__content\"><ng-content /></div>\n </main>\n <!-- One outer block, so a page without a right column keeps 3.27.2's single\n anchor (an @else branch adds one); the reserved column (AS-35) is empty,\n aria-hidden and no landmark. -->\n @if (hasAside() || reserved()) {\n @if (hasAside()) {\n <aside\n class=\"lc-app-page__aside\"\n [attr.aria-labelledby]=\"asideLabelledby() || null\"\n [attr.aria-label]=\"asideLabelledby() ? null : asideAriaLabel()\"\n >\n <ng-content select=\"[page-aside]\" />\n </aside>\n } @else {<div class=\"lc-app-page__aside lc-app-page__aside--reserved\" aria-hidden=\"true\"></div>}\n }\n</div>\n",
302
+ styles: [":host{display:block;min-width:0}.lc-app-page{--_pad-x: var(--lc-app-page-padding-x, 24px);--_pad-top: var(--lc-app-page-padding-top, 24px);--_gap: var(--lc-app-page-gap, 24px);--_aside-width: var(--lc-app-page-aside-width, 360px);display:grid;grid-template-columns:minmax(0,1fr);min-height:100%}.lc-app-page__main{display:block;min-width:0;padding:var(--_pad-top) var(--_pad-x) 16px}.lc-app-page__content{display:flex;flex-direction:column;gap:var(--_gap);max-width:var(--lc-app-page-content-max, 880px);margin-inline:auto}.lc-app-page__aside{display:none}@media(min-width:640px){.lc-app-page{--_pad-x: var(--lc-app-page-padding-x, 32px);--_pad-top: var(--lc-app-page-padding-top, 32px)}}@media(min-width:1024px){.lc-app-page{--_pad-x: var(--lc-app-page-padding-x, 48px);--_pad-top: var(--lc-app-page-padding-top, 48px);--_gap: var(--lc-app-page-gap, 40px)}.lc-app-page__main{padding-bottom:32px}}@media(min-width:1280px){.lc-app-page{--_pad-x: var(--lc-app-page-padding-x, 56px);--_pad-top: var(--lc-app-page-padding-top, 56px)}.lc-app-page--aside{grid-template-columns:minmax(0,1fr) var(--_aside-width)}.lc-app-page__aside{display:block;min-width:0;box-sizing:border-box;padding:max(16px,var(--_pad-top) - 32px) 40px 40px;background:linear-gradient(180deg,var(--color-background-low),transparent);border-left:1px solid var(--color-hairline, var(--color-border));border-image:linear-gradient(180deg,var(--color-hairline, var(--color-border)) 60%,transparent) 1}}@media(min-width:1440px){.lc-app-page{--_pad-x: var(--lc-app-page-padding-x, 72px);--_pad-top: var(--lc-app-page-padding-top, 72px);--_aside-width: var(--lc-app-page-aside-width, 400px)}}@media(min-width:1920px){.lc-app-page{--_pad-x: var(--lc-app-page-padding-x, 96px);--_pad-top: var(--lc-app-page-padding-top, 112px);--_aside-width: var(--lc-app-page-aside-width, 460px)}}.lc-app-page.lc-app-page--pad-x-header{--_pad-x: var(--lc-app-page-padding-x, var(--lc-density-padding-lg, var(--spacing-6)))}@media(max-width:768px){.lc-app-page.lc-app-page--pad-x-header{--_pad-x: var(--lc-app-page-padding-x, var(--lc-density-padding-md, var(--spacing-4)))}}.lc-app-page.lc-app-page--pad-top-none{--_pad-top: var(--lc-app-page-padding-top, 0px)}.lc-app-page.lc-app-page--pad-top-sm{--_pad-top: var(--lc-app-page-padding-top, var(--spacing-2))}.lc-app-page.lc-app-page--pad-top-md{--_pad-top: var(--lc-app-page-padding-top, var(--spacing-4))}@media(forced-colors:active){.lc-app-page__aside{background:none;border-image:none;border-left-color:CanvasText}}@media(min-width:1280px){:host ::ng-deep .lc-app-page--wide>.lc-app-page__main>.lc-app-page__content>[page-wide]{box-sizing:border-box;width:min(var(--_lc-app-page-wide-max, 928px),100% + var(--_aside-width));max-width:none}}\n"]
241
303
  }]
242
304
  }],
243
305
  propDecorators: {
@@ -296,6 +358,22 @@ i0.ɵɵngDeclareClassMetadata({
296
358
  alias: "paddingTop",
297
359
  required: false
298
360
  }]
361
+ }],
362
+ reserveAside: [{
363
+ type: i0.Input,
364
+ args: [{
365
+ isSignal: true,
366
+ alias: "reserveAside",
367
+ required: false
368
+ }]
369
+ }],
370
+ contentWidth: [{
371
+ type: i0.Input,
372
+ args: [{
373
+ isSignal: true,
374
+ alias: "contentWidth",
375
+ required: false
376
+ }]
299
377
  }]
300
378
  }
301
379
  });
@@ -1 +1 @@
1
- {"version":3,"file":"lifecorner-angular-ui-kit-app-shell.mjs","names":[],"sources":["../../../../libs/angular-ui-kit/src/lib/app-shell/app-shell.component.ts","../../../../libs/angular-ui-kit/src/lib/app-shell/app-shell.component.html","../../../../libs/angular-ui-kit/src/lib/app-shell/app-page.component.ts","../../../../libs/angular-ui-kit/src/lib/app-shell/app-page.component.html"],"sourcesContent":["import {\n afterNextRender,\n ChangeDetectionStrategy,\n Component,\n computed,\n DestroyRef,\n ElementRef,\n inject,\n input,\n viewChild,\n} from '@angular/core';\n\n/**\n * Private custom property the shell writes on `.lc-app-shell` in the browser:\n * the bar's content height. Its CSS fallback is 72 px (4 + 64 + 4 at 100 % text).\n */\nconst BAR_VAR = '--_lc-app-shell-bar';\n\n/**\n * Where the side navigation sits from 1024 px: `'below-header'` (the header\n * spans the full width, the nav starts below it) or `'full-height'` (the nav\n * runs from the top to the bottom of the viewport, the header beside it).\n */\nexport type AppShellNavPlacement = 'below-header' | 'full-height';\n\n/**\n * App shell: the frame around every page of an app — header, side navigation,\n * page, bottom navigation bar, footer and the horizon — laid out for phones\n * first, with the DOCUMENT scrolling (not an inner container), so the iOS\n * status-bar tap, address-bar collapse and pull-to-refresh keep working.\n *\n * Slots (content projection by attribute):\n * - `[shell-header]` — the `lc-header`; scrolls away with the page. Gets the\n * top, left and right safe-area insets.\n * - `[shell-nav]` — the docked side navigation (`lc-sidenav mode=\"docked\"\n * [isOpen]=\"true\" width=\"100%\" [mobileBreakpoint]=\"0\"`), shown from\n * 1024 px in a column of 240 px (248 from 1440, 280 from 1920; override\n * with `--lc-app-shell-nav-width`) and sticky once the header has gone.\n * An empty slot takes no column.\n * - default — the page, usually an `lc-app-page` (or a `<router-outlet>`\n * whose pages use one). It spans to the window's right edge, so a page's\n * right column docks there.\n * - `[shell-bottom-nav]` — the `lc-bottom-nav`, shown below 1024 px. The shell\n * reserves its real height at the end of the page: at least 112 px (16 + 72\n * + 24) plus the safe-area inset, more when the bar grows with the text size\n * (measured in the browser; `--lc-app-shell-bottom-clearance` overrides it),\n * so it never covers content. With the horizon, the bar rests on the land\n * instead, and the land grows with a taller bar (3.10.1, AS-31).\n * - `[shell-footer]` — e.g. a compact `lc-footer`; after the page, or with\n * `horizon` in a row of its own below the horizon, painted in the land's\n * colour (`--color-land-3`): the land grows by the footer's height, so\n * `--lc-horizon-land` stays the land above the footer and no page overrides\n * it (3.9, LK-12).\n * - `[shell-horizon-figure]` — Edu (or any decorative figure) on the horizon\n * line, left of the sun: `lc-character`, `<img>`, `<picture>` or an element\n * holding an inline `<svg>`. Projected as a direct child of `.lc-horizon`,\n * sized and placed by the horizon (`--lc-horizon-figure-size` / `-gap`).\n * Decorative: the horizon is `aria-hidden`, so give an `<img>` `alt=\"\"` and\n * nothing focusable. Shown only when `horizon` is on (3.9, AS-29).\n *\n * `horizon` closes the page with the \"Weite\" horizon (`.lc-horizon` from the\n * kit's styles): full-bleed, below the side navigation and the page, filling\n * the rest of the screen — on Today, the sign-in pages and the shell pages.\n *\n * `navPlacement=\"full-height\"` (3.14) runs the nav column from the top to the\n * bottom of the viewport (sticky, 100dvh) with the header beside it, from\n * 1024 px; below 1024 px it renders like the default. The DOM and tab order\n * stay header → nav → page → footer, so in full-height the header's controls\n * come before a brand at the top of the nav (banner, then navigation, then\n * content). The land and the footer run under the nav; the footer's content\n * starts at the nav width.\n *\n * CSS hooks (set them on `lc-app-shell` or any ancestor):\n * - `--lc-app-shell-nav-width` — the nav column from 1024 px. Default\n * `240px` (`248px` from 1440, `280px` from 1920).\n * - `--lc-app-shell-bottom-clearance` — the space kept free for the bottom\n * bar below 1024 px. Default: the bar's height + 40 px + the inset, at\n * least `112px` + the inset.\n * - `--lc-app-shell-bg` — the page colour behind everything (the sky).\n * Default `var(--color-background)`.\n * - `--lc-app-shell-bg-image` — a layer over the page colour. Default\n * `var(--app-background-image, none)`. A tint, for example:\n * `linear-gradient(to bottom, var(--color-land-1), var(--color-land-2), var(--color-land-3))`.\n * The horizon's land still paints over it.\n *\n * Needs `<meta name=\"viewport\" content=\"width=device-width, initial-scale=1,\n * viewport-fit=cover\">` for the safe-area insets. Breakpoints are fixed\n * (1024 px side navigation, 1440 / 1920 px widths), so the layout works\n * before JavaScript runs (SSR, prerendering).\n *\n * @example\n * ```html\n * <lc-app-shell [horizon]=\"isToday()\">\n * <lc-header shell-header variant=\"transparent\" [sticky]=\"false\" … />\n * <lc-sidenav shell-nav mode=\"docked\" [isOpen]=\"true\" width=\"100%\" [mobileBreakpoint]=\"0\"\n * variant=\"transparent\" [items]=\"nav\" [activeRoute]=\"url()\" />\n * <router-outlet />\n * <lc-bottom-nav shell-bottom-nav variant=\"floating\" [items]=\"nav\" [activeRoute]=\"url()\" />\n * </lc-app-shell>\n * ```\n */\n@Component({\n selector: 'lc-app-shell',\n standalone: true,\n templateUrl: './app-shell.component.html',\n styleUrl: './app-shell.component.scss',\n changeDetection: ChangeDetectionStrategy.OnPush,\n})\nexport class AppShellComponent {\n /**\n * Close the page with the horizon (`.lc-horizon`): full width, below the\n * side navigation and the page, filling the rest of the screen; the footer\n * (in a row of its own, which adds to the land) and the floating bar rest\n * on its land, and `[shell-horizon-figure]` stands on its line.\n * @default false\n */\n readonly horizon = input(false);\n\n /**\n * Where the side navigation sits from 1024 px. `'below-header'`: the header\n * spans the full width and the nav starts below it. `'full-height'`: the nav\n * runs from the top to the bottom of the viewport, with the header beside\n * it. Below 1024 px both render the same. The DOM and tab order don't change\n * (header, nav, page). CSS only, so it holds before JavaScript runs.\n * `'full-height'` expects a brand block at the top of the rail\n * (`lc-sidenav brandName` or `showLogo`, with a `logoSize` that matches the\n * header's `size`, e.g. both `md`: 64 px) and the header's `showLogo` off.\n * @default 'below-header'\n */\n readonly navPlacement = input<AppShellNavPlacement>('below-header');\n\n protected readonly shellClasses = computed(() => {\n const classes = ['lc-app-shell'];\n if (this.horizon()) classes.push('lc-app-shell--horizon');\n if (this.navPlacement() === 'full-height') classes.push('lc-app-shell--nav-full-height');\n return classes.join(' ');\n });\n\n private readonly shell = viewChild.required<ElementRef<HTMLElement>>('shell');\n private readonly barSlot = viewChild.required<ElementRef<HTMLElement>>('barSlot');\n\n constructor() {\n const destroyRef = inject(DestroyRef);\n // Browser only (afterNextRender never runs on the server): follow the\n // bar's content height, so the clearance and the land grow with a bar\n // that grows with the text size (AS-31). Without ResizeObserver, and\n // before this runs, the CSS fallback (72 px → 112 px clearance) holds.\n afterNextRender(() => {\n if (typeof ResizeObserver === 'undefined') return;\n const shell = this.shell().nativeElement;\n const slot = this.barSlot().nativeElement;\n const clear = () => shell.style.removeProperty(BAR_VAR);\n const sizes = new ResizeObserver((entries) => {\n const entry = entries[entries.length - 1];\n const height = entry.contentBoxSize?.[0]?.blockSize ?? entry.contentRect.height;\n // 0 when the bar is hidden (from 1024 px).\n if (height > 0) shell.style.setProperty(BAR_VAR, `${height}px`);\n else clear();\n });\n let bar: Element | null = null;\n // The bar may come later (`@if (items())` around it) or go again.\n const pick = () => {\n const next = slot.querySelector('.lc-bottom-nav') ?? slot.firstElementChild;\n if (next === bar) return;\n if (bar) sizes.unobserve(bar);\n bar = next;\n if (bar) sizes.observe(bar);\n else clear();\n };\n pick();\n const changes = typeof MutationObserver === 'undefined' ? null : new MutationObserver(pick);\n changes?.observe(slot, { childList: true, subtree: true });\n destroyRef.onDestroy(() => {\n sizes.disconnect();\n changes?.disconnect();\n });\n });\n }\n}\n","<div #shell [class]=\"shellClasses()\">\n <div class=\"lc-app-shell__header\"><ng-content select=\"[shell-header]\" /></div>\n <div class=\"lc-app-shell__nav\"><ng-content select=\"[shell-nav]\" /></div>\n <div class=\"lc-app-shell__body\"><ng-content /></div>\n @if (horizon()) {\n <div class=\"lc-app-shell__horizon lc-horizon\" aria-hidden=\"true\"><ng-content select=\"[shell-horizon-figure]\" /></div>\n }\n <div class=\"lc-app-shell__footer\"><ng-content select=\"[shell-footer]\" /></div>\n <div #barSlot class=\"lc-app-shell__bottom-nav\"><ng-content select=\"[shell-bottom-nav]\" /></div>\n</div>\n","import { ChangeDetectionStrategy, Component, computed, input } from '@angular/core';\n\n/**\n * Horizontal padding of `lc-app-page` (3.15).\n * - `default`: 24 · 32 · 48 · 56 · 72 · 96 px from 0 · 640 · 1024 · 1280 · 1440 · 1920 px.\n * - `header`: `lc-header`'s padding (16 px up to 768 px, 24 px above), so the\n * content lines up with the header title and the user menu.\n */\nexport type AppPagePaddingX = 'default' | 'header';\n\n/**\n * Space above `lc-app-page`'s content (3.15).\n * - `default`: 24 · 32 · 48 · 56 · 72 · 112 px by width.\n * - `none` 0, `sm` 8 px, `md` 16 px at every width.\n */\nexport type AppPagePaddingTop = 'default' | 'none' | 'sm' | 'md';\n\n/**\n * A page inside `lc-app-shell`: the `<main>` landmark with the page's content\n * centred at a reading width, and an optional right column docked to the\n * window's right edge (LK-6).\n *\n * - `<main [id]=\"mainId\">` holds the default slot as a column (24 px apart,\n * 40 px from 1024 px), centred and capped at `contentMaxWidth` (880 px of\n * content). Side padding 24 · 32 · 48 · 56 · 72 · 96 px at 360 · 640 ·\n * 1024 · 1280 · 1440 · 1920 px.\n * - `[page-aside]` is the right column: an `<aside>` AFTER `<main>` in the DOM,\n * shown from 1280 px at 360 px (400 from 1440, 460 from 1920; override\n * with `--lc-app-page-aside-width`), with a tint and a hairline that fade\n * downwards; inside `lc-app-shell` they start at the top of the window,\n * behind a transparent header. Below 1280 px it is hidden: show the page's narrow equivalent\n * inside main instead (e.g. a member row). It renders only with an\n * accessible name — `asideLabelledby` (the id of its heading) or\n * `asideAriaLabel`.\n * - `busy` marks the page as loading: `aria-busy=\"true\"` on `<main>` while a\n * skeleton stands in for the content (AS-12). No visual change.\n * - `paddingX=\"header\"` lines the content up with `lc-header` (3.15): its\n * title and its user menu. Combine it with `contentMaxWidth=\"none\"` (or a\n * page narrower than the cap); centred content doesn't align.\n * `paddingTop` sets the space above the content (`none`, `sm` 8 px, `md`\n * 16 px), e.g. `md` with `lc-page-header paddingTop=\"none\"` puts the `h1`\n * 16 px below the header.\n *\n * CSS hooks (set them on `lc-app-page` or any ancestor; each wins over the\n * inputs and holds one value at every width):\n * - `--lc-app-page-padding-x`: the side padding (default: `paddingX`).\n * Per breakpoint, set it inside your own media query:\n * `lc-app-page { --lc-app-page-padding-x: 16px }` and\n * `@media (min-width: 1024px) { lc-app-page { --lc-app-page-padding-x: 32px } }`.\n * - `--lc-app-page-padding-top`: the space above the content (default: `paddingTop`).\n * - `--lc-app-page-gap`: the space between the content's children (24 px, 40 px from 1024 px).\n * - `--lc-app-page-aside-width`: the right column (360 px, 400 from 1440, 460 from 1920).\n *\n * @example\n * ```html\n * <lc-app-page asideLabelledby=\"members-title\">\n * <lc-page-header … />\n * <lc-next-step-card … />\n * <section page-aside>\n * <h2 id=\"members-title\">Mitglieder</h2>\n * <lc-list [items]=\"members\" />\n * </section>\n * </lc-app-page>\n * ```\n */\n@Component({\n selector: 'lc-app-page',\n standalone: true,\n templateUrl: './app-page.component.html',\n styleUrl: './app-page.component.scss',\n changeDetection: ChangeDetectionStrategy.OnPush,\n})\nexport class AppPageComponent {\n /** Id of the `<main>` element (the skip link's target). */\n readonly mainId = input('main');\n\n /**\n * Widest the page content gets (CSS length; `none` for no cap). The default\n * keeps lines readable on wide screens: the page gains air, not line length.\n */\n readonly contentMaxWidth = input('880px');\n\n /** Id of the heading that names the right column (`aria-labelledby`). */\n readonly asideLabelledby = input('');\n\n /** Accessible name of the right column when it has no visible heading. */\n readonly asideAriaLabel = input('');\n\n /**\n * Marks the page as loading: `aria-busy=\"true\"` on its `<main>` while\n * `true`, no attribute while `false`. Set it while a skeleton stands in for\n * the content. Screen readers may hold back changes inside a busy region\n * until it clears, so a loading message that must be heard belongs outside\n * `<main>` (for example a `.sr-only` `role=\"status\"` next to the page) or is\n * static text. No visual change.\n * @default false\n */\n readonly busy = input(false);\n\n /**\n * Horizontal page padding. `'default'`: 24/32/48/56/72/96 px from\n * 0/640/1024/1280/1440/1920 px. `'header'`: the same padding as `lc-header`\n * (16 px up to 768 px, 24 px above, from `--lc-density-padding-md`/`-lg`),\n * so the content lines up with the header title and the user menu. Combine\n * it with `contentMaxWidth=\"none\"` (or a page narrower than the cap).\n * `--lc-app-page-padding-x` wins over both.\n * @default 'default'\n */\n readonly paddingX = input<AppPagePaddingX>('default');\n\n /**\n * Space above the page content. `'default'`: 24/32/48/56/72/112 px by\n * width. `'none'` 0, `'sm'` 8 px, `'md'` 16 px at every width.\n * `--lc-app-page-padding-top` wins.\n * @default 'default'\n */\n readonly paddingTop = input<AppPagePaddingTop>('default');\n\n /** The right column renders only when it has an accessible name. */\n protected readonly hasAside = computed(() => !!(this.asideLabelledby() || this.asideAriaLabel()));\n\n protected readonly pageClasses = computed(() => {\n const classes = ['lc-app-page'];\n if (this.hasAside()) classes.push('lc-app-page--aside');\n if (this.paddingX() === 'header') classes.push('lc-app-page--pad-x-header');\n if (this.paddingTop() !== 'default') classes.push(`lc-app-page--pad-top-${this.paddingTop()}`);\n return classes.join(' ');\n });\n}\n","<div [class]=\"pageClasses()\" [style.--lc-app-page-content-max]=\"contentMaxWidth()\">\n <main class=\"lc-app-page__main\" [id]=\"mainId()\" [attr.aria-busy]=\"busy() ? 'true' : null\">\n <div class=\"lc-app-page__content\"><ng-content /></div>\n </main>\n @if (hasAside()) {\n <aside\n class=\"lc-app-page__aside\"\n [attr.aria-labelledby]=\"asideLabelledby() || null\"\n [attr.aria-label]=\"asideLabelledby() ? null : asideAriaLabel()\"\n >\n <ng-content select=\"[page-aside]\" />\n </aside>\n }\n</div>\n"],"mappings":"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"}
1
+ {"version":3,"file":"lifecorner-angular-ui-kit-app-shell.mjs","names":[],"sources":["../../../../libs/angular-ui-kit/src/lib/app-shell/app-shell.component.ts","../../../../libs/angular-ui-kit/src/lib/app-shell/app-shell.component.html","../../../../libs/angular-ui-kit/src/lib/app-shell/app-page.component.ts","../../../../libs/angular-ui-kit/src/lib/app-shell/app-page.component.html"],"sourcesContent":["import {\n afterNextRender,\n ChangeDetectionStrategy,\n Component,\n computed,\n DestroyRef,\n ElementRef,\n inject,\n input,\n viewChild,\n} from '@angular/core';\nimport { OverlayInsetsService } from '@lifecorner/angular-ui-kit/internal/overlay';\n\n/**\n * Private custom property the shell writes on `.lc-app-shell` in the browser:\n * the bar's content height. Its CSS fallback is 72 px (4 + 64 + 4 at 100 % text).\n */\nconst BAR_VAR = '--_lc-app-shell-bar';\n\n/** What a fixed overlay keeps free without a header or without a shown bar (AS-34). */\nconst TOP_INSET = 'env(safe-area-inset-top, 0px)';\nconst BOTTOM_INSET = 'env(safe-area-inset-bottom, 0px)';\n\n/** The border-box height of a ResizeObserver entry, with a layout read where the browser doesn't report it. */\nconst borderBoxHeight = (entry: ResizeObserverEntry, target: Element) =>\n entry.borderBoxSize?.[0]?.blockSize ?? target.getBoundingClientRect().height;\n\n/**\n * Where the side navigation sits from 1024 px: `'below-header'` (the header\n * spans the full width, the nav starts below it) or `'full-height'` (the nav\n * runs from the top to the bottom of the viewport, the header beside it).\n */\nexport type AppShellNavPlacement = 'below-header' | 'full-height';\n\n/**\n * App shell: the frame around every page of an app — header, side navigation,\n * page, bottom navigation bar, footer and the horizon — laid out for phones\n * first, with the DOCUMENT scrolling (not an inner container), so the iOS\n * status-bar tap, address-bar collapse and pull-to-refresh keep working.\n *\n * Slots (content projection by attribute):\n * - `[shell-header]` — the `lc-header`; scrolls away with the page. Gets the\n * top, left and right safe-area insets.\n * - `[shell-nav]` — the docked side navigation (`lc-sidenav mode=\"docked\"\n * [isOpen]=\"true\" width=\"100%\" [mobileBreakpoint]=\"0\"`), shown from\n * 1024 px in a column of 240 px (248 from 1440, 280 from 1920; override\n * with `--lc-app-shell-nav-width`) and sticky once the header has gone.\n * An empty slot takes no column.\n * - default — the page, usually an `lc-app-page` (or a `<router-outlet>`\n * whose pages use one). It spans to the window's right edge, so a page's\n * right column docks there.\n * - `[shell-bottom-nav]` — the `lc-bottom-nav`, shown below 1024 px. The shell\n * reserves its real height at the end of the page: at least 112 px (16 + 72\n * + 24) plus the safe-area inset, more when the bar grows with the text size\n * (measured in the browser; `--lc-app-shell-bottom-clearance` overrides it),\n * so it never covers content. With the horizon, the bar rests on the land\n * instead, and the land grows with a taller bar (3.10.1, AS-31).\n * - `[shell-footer]` — e.g. a compact `lc-footer`; after the page, or with\n * `horizon` in a row of its own below the horizon, painted in the land's\n * colour (`--color-land-3`): the land grows by the footer's height, so\n * `--lc-horizon-land` stays the land above the footer and no page overrides\n * it (3.9, LK-12).\n * - `[shell-horizon-figure]` — Edu (or any decorative figure) on the horizon\n * line, left of the sun: `lc-character`, `<img>`, `<picture>` or an element\n * holding an inline `<svg>`. Projected as a direct child of `.lc-horizon`,\n * sized and placed by the horizon (`--lc-horizon-figure-size` / `-gap`).\n * Decorative: the horizon is `aria-hidden`, so give an `<img>` `alt=\"\"` and\n * nothing focusable. Shown only when `horizon` is on (3.9, AS-29).\n *\n * `horizon` closes the page with the \"Weite\" horizon (`.lc-horizon` from the\n * kit's styles): full-bleed, below the side navigation and the page, filling\n * the rest of the screen — on Today, the sign-in pages and the shell pages.\n *\n * `navPlacement=\"full-height\"` (3.14) runs the nav column from the top to the\n * bottom of the viewport (sticky, 100dvh) with the header beside it, from\n * 1024 px; below 1024 px it renders like the default. The DOM and tab order\n * stay header → nav → page → footer, so in full-height the header's controls\n * come before a brand at the top of the nav (banner, then navigation, then\n * content). The land and the footer run under the nav; the footer's content\n * starts at the nav width.\n *\n * Toasts (3.28, AS-34): in the browser the shell tells `lc-toast-outlet` —\n * wherever the app puts it, also after `</lc-app-shell>` — where its header\n * ends and where the shown bar starts, so top toasts sit below the header and\n * bottom toasts 16 px above the bar (from 1024 px: above the safe-area\n * inset). `--lc-toast-outlet-offset-top` / `-bottom` override both.\n *\n * CSS hooks (set them on `lc-app-shell` or any ancestor):\n * - `--lc-app-shell-nav-width` — the nav column from 1024 px. Default\n * `240px` (`248px` from 1440, `280px` from 1920).\n * - `--lc-app-shell-bottom-clearance` — the space kept free for the bottom\n * bar below 1024 px. Default: the bar's height + 40 px + the inset, at\n * least `112px` + the inset.\n * - `--lc-app-shell-bg` — the page colour behind everything (the sky).\n * Default `var(--color-background)`.\n * - `--lc-app-shell-bg-image` — a layer over the page colour. Default\n * `var(--app-background-image, none)`. A tint, for example:\n * `linear-gradient(to bottom, var(--color-land-1), var(--color-land-2), var(--color-land-3))`.\n * The horizon's land still paints over it.\n *\n * Needs `<meta name=\"viewport\" content=\"width=device-width, initial-scale=1,\n * viewport-fit=cover\">` for the safe-area insets. Breakpoints are fixed\n * (1024 px side navigation, 1440 / 1920 px widths), so the layout works\n * before JavaScript runs (SSR, prerendering).\n *\n * @example\n * ```html\n * <lc-app-shell [horizon]=\"isToday()\">\n * <lc-header shell-header variant=\"transparent\" [sticky]=\"false\" … />\n * <lc-sidenav shell-nav mode=\"docked\" [isOpen]=\"true\" width=\"100%\" [mobileBreakpoint]=\"0\"\n * variant=\"transparent\" [items]=\"nav\" [activeRoute]=\"url()\" />\n * <router-outlet />\n * <lc-bottom-nav shell-bottom-nav variant=\"floating\" [items]=\"nav\" [activeRoute]=\"url()\" />\n * </lc-app-shell>\n * ```\n */\n@Component({\n selector: 'lc-app-shell',\n standalone: true,\n templateUrl: './app-shell.component.html',\n styleUrl: './app-shell.component.scss',\n changeDetection: ChangeDetectionStrategy.OnPush,\n})\nexport class AppShellComponent {\n /**\n * Close the page with the horizon (`.lc-horizon`): full width, below the\n * side navigation and the page, filling the rest of the screen; the footer\n * (in a row of its own, which adds to the land) and the floating bar rest\n * on its land, and `[shell-horizon-figure]` stands on its line.\n * @default false\n */\n readonly horizon = input(false);\n\n /**\n * Where the side navigation sits from 1024 px. `'below-header'`: the header\n * spans the full width and the nav starts below it. `'full-height'`: the nav\n * runs from the top to the bottom of the viewport, with the header beside\n * it. Below 1024 px both render the same. The DOM and tab order don't change\n * (header, nav, page). CSS only, so it holds before JavaScript runs.\n * `'full-height'` expects a brand block at the top of the rail\n * (`lc-sidenav brandName` or `showLogo`, with a `logoSize` that matches the\n * header's `size`, e.g. both `md`: 64 px) and the header's `showLogo` off.\n * @default 'below-header'\n */\n readonly navPlacement = input<AppShellNavPlacement>('below-header');\n\n protected readonly shellClasses = computed(() => {\n const classes = ['lc-app-shell'];\n if (this.horizon()) classes.push('lc-app-shell--horizon');\n if (this.navPlacement() === 'full-height') classes.push('lc-app-shell--nav-full-height');\n return classes.join(' ');\n });\n\n private readonly shell = viewChild.required<ElementRef<HTMLElement>>('shell');\n private readonly headerSlot = viewChild.required<ElementRef<HTMLElement>>('headerSlot');\n private readonly barSlot = viewChild.required<ElementRef<HTMLElement>>('barSlot');\n\n constructor() {\n const destroyRef = inject(DestroyRef);\n const insets = inject(OverlayInsetsService);\n // Browser only (afterNextRender never runs on the server): follow the\n // bar's content height, so the clearance and the land grow with a bar\n // that grows with the text size (AS-31). Without ResizeObserver, and\n // before this runs, the CSS fallback (72 px → 112 px clearance) holds.\n // The same measurements tell a fixed overlay (lc-toast-outlet, AS-34)\n // what to keep free: the header's bottom edge and the bar's top edge.\n afterNextRender(() => {\n if (typeof ResizeObserver === 'undefined') return;\n const shell = this.shell().nativeElement;\n const header = this.headerSlot().nativeElement;\n const slot = this.barSlot().nativeElement;\n // Until a box is measured (a hidden bar or an empty header reports\n // nothing): the safe-area insets.\n insets.top.set(TOP_INSET);\n insets.bottom.set(BOTTOM_INSET);\n let bar: Element | null = null;\n const clear = () => {\n shell.style.removeProperty(BAR_VAR);\n insets.bottom.set(BOTTOM_INSET);\n };\n const sizes = new ResizeObserver((entries) => {\n const entry = entries[entries.length - 1];\n const height = entry.contentBoxSize?.[0]?.blockSize ?? entry.contentRect.height;\n // 0 when the bar is hidden (from 1024 px).\n if (height > 0 && bar) {\n shell.style.setProperty(BAR_VAR, `${height}px`);\n // Its top edge from the viewport's bottom: its own `bottom` (16 px\n // plus the inset when floating) plus its border box.\n const bottom = parseFloat(getComputedStyle(bar).bottom);\n insets.bottom.set(`${(Number.isFinite(bottom) ? bottom : 0) + borderBoxHeight(entry, bar)}px`);\n } else clear();\n });\n // The header wrapper carries the top inset (its padding), so its border\n // box ends at the header's bottom edge; 0 when the slot is empty.\n const headerSizes = new ResizeObserver((entries) => {\n const height = borderBoxHeight(entries[entries.length - 1], header);\n insets.top.set(height > 0 ? `${height}px` : TOP_INSET);\n });\n headerSizes.observe(header);\n // The bar may come later (`@if (items())` around it) or go again.\n const pick = () => {\n const next = slot.querySelector('.lc-bottom-nav') ?? slot.firstElementChild;\n if (next === bar) return;\n if (bar) sizes.unobserve(bar);\n bar = next;\n if (bar) sizes.observe(bar);\n else clear();\n };\n pick();\n const changes = typeof MutationObserver === 'undefined' ? null : new MutationObserver(pick);\n changes?.observe(slot, { childList: true, subtree: true });\n destroyRef.onDestroy(() => {\n sizes.disconnect();\n headerSizes.disconnect();\n changes?.disconnect();\n insets.top.set(null);\n insets.bottom.set(null);\n });\n });\n }\n}\n","<div #shell [class]=\"shellClasses()\">\n <div #headerSlot class=\"lc-app-shell__header\"><ng-content select=\"[shell-header]\" /></div>\n <div class=\"lc-app-shell__nav\"><ng-content select=\"[shell-nav]\" /></div>\n <div class=\"lc-app-shell__body\"><ng-content /></div>\n @if (horizon()) {\n <div class=\"lc-app-shell__horizon lc-horizon\" aria-hidden=\"true\"><ng-content select=\"[shell-horizon-figure]\" /></div>\n }\n <div class=\"lc-app-shell__footer\"><ng-content select=\"[shell-footer]\" /></div>\n <div #barSlot class=\"lc-app-shell__bottom-nav\"><ng-content select=\"[shell-bottom-nav]\" /></div>\n</div>\n","import { ChangeDetectionStrategy, Component, computed, input } from '@angular/core';\n\n/**\n * Horizontal padding of `lc-app-page` (3.15).\n * - `default`: 24 · 32 · 48 · 56 · 72 · 96 px from 0 · 640 · 1024 · 1280 · 1440 · 1920 px.\n * - `header`: `lc-header`'s padding (16 px up to 768 px, 24 px above), so the\n * content lines up with the header title and the user menu.\n */\nexport type AppPagePaddingX = 'default' | 'header';\n\n/**\n * Space above `lc-app-page`'s content (3.15).\n * - `default`: 24 · 32 · 48 · 56 · 72 · 112 px by width.\n * - `none` 0, `sm` 8 px, `md` 16 px at every width.\n */\nexport type AppPagePaddingTop = 'default' | 'none' | 'sm' | 'md';\n\n/**\n * Width of `lc-app-page`'s content from 1280 px (3.28, AS-36).\n * - `default`: the content column, centred, capped at `contentMaxWidth`.\n * - `wide`: the standard column of a page with a right column, the right\n * column left empty, and `[page-wide]` children running over it, up to\n * 928 px. Below 1280 px, and next to a named right column, as `default`.\n */\nexport type AppPageContentWidth = 'default' | 'wide';\n\n/**\n * A page inside `lc-app-shell`: the `<main>` landmark with the page's content\n * centred at a reading width, and an optional right column docked to the\n * window's right edge (LK-6).\n *\n * - `<main [id]=\"mainId\">` holds the default slot as a column (24 px apart,\n * 40 px from 1024 px), centred and capped at `contentMaxWidth` (880 px of\n * content). Side padding 24 · 32 · 48 · 56 · 72 · 96 px at 360 · 640 ·\n * 1024 · 1280 · 1440 · 1920 px.\n * - `[page-aside]` is the right column: an `<aside>` AFTER `<main>` in the DOM,\n * shown from 1280 px at 360 px (400 from 1440, 460 from 1920; override\n * with `--lc-app-page-aside-width`), with a tint and a hairline that fade\n * downwards; inside `lc-app-shell` they start at the top of the window,\n * behind a transparent header. Below 1280 px it is hidden: show the page's narrow equivalent\n * inside main instead (e.g. a member row). It renders only with an\n * accessible name — `asideLabelledby` (the id of its heading) or\n * `asideAriaLabel`.\n * - `reserveAside` keeps that column from 1280 px — width, tint and hairline —\n * with no content and no landmark (`aria-hidden=\"true\"`), so a page without\n * right-column content keeps the same column as a page with one (3.28,\n * AS-35). A named aside wins.\n * - `contentWidth=\"wide\"` (3.28, AS-36) lays the page out like a page with a\n * right column, leaves the column empty and untinted, and lets children\n * marked `[page-wide]` (e.g. a table's wrapper) run over it, up to 928 px;\n * the other children (the page header, the next-step card) keep the\n * standard column, so the title doesn't move between pages. Next to a named\n * aside, and below 1280 px, it renders like `'default'`.\n * - `busy` marks the page as loading: `aria-busy=\"true\"` on `<main>` while a\n * skeleton stands in for the content (AS-12). No visual change.\n * - `paddingX=\"header\"` lines the content up with `lc-header` (3.15): its\n * title and its user menu. Combine it with `contentMaxWidth=\"none\"` (or a\n * page narrower than the cap); centred content doesn't align.\n * `paddingTop` sets the space above the content (`none`, `sm` 8 px, `md`\n * 16 px), e.g. `md` with `lc-page-header paddingTop=\"none\"` puts the `h1`\n * 16 px below the header.\n *\n * CSS hooks (set them on `lc-app-page` or any ancestor; each wins over the\n * inputs and holds one value at every width):\n * - `--lc-app-page-padding-x`: the side padding (default: `paddingX`).\n * Per breakpoint, set it inside your own media query:\n * `lc-app-page { --lc-app-page-padding-x: 16px }` and\n * `@media (min-width: 1024px) { lc-app-page { --lc-app-page-padding-x: 32px } }`.\n * - `--lc-app-page-padding-top`: the space above the content (default: `paddingTop`).\n * - `--lc-app-page-gap`: the space between the content's children (24 px, 40 px from 1024 px).\n * - `--lc-app-page-aside-width`: the right column (360 px, 400 from 1440, 460 from 1920).\n *\n * @example\n * ```html\n * <lc-app-page asideLabelledby=\"members-title\">\n * <lc-page-header … />\n * <lc-next-step-card … />\n * <section page-aside>\n * <h2 id=\"members-title\">Mitglieder</h2>\n * <lc-list [items]=\"members\" />\n * </section>\n * </lc-app-page>\n * ```\n */\n@Component({\n selector: 'lc-app-page',\n standalone: true,\n templateUrl: './app-page.component.html',\n styleUrl: './app-page.component.scss',\n changeDetection: ChangeDetectionStrategy.OnPush,\n})\nexport class AppPageComponent {\n /** Id of the `<main>` element (the skip link's target). */\n readonly mainId = input('main');\n\n /**\n * Widest the page content gets (CSS length; `none` for no cap). The default\n * keeps lines readable on wide screens: the page gains air, not line length.\n */\n readonly contentMaxWidth = input('880px');\n\n /** Id of the heading that names the right column (`aria-labelledby`). */\n readonly asideLabelledby = input('');\n\n /** Accessible name of the right column when it has no visible heading. */\n readonly asideAriaLabel = input('');\n\n /**\n * Marks the page as loading: `aria-busy=\"true\"` on its `<main>` while\n * `true`, no attribute while `false`. Set it while a skeleton stands in for\n * the content. Screen readers may hold back changes inside a busy region\n * until it clears, so a loading message that must be heard belongs outside\n * `<main>` (for example a `.sr-only` `role=\"status\"` next to the page) or is\n * static text. No visual change.\n * @default false\n */\n readonly busy = input(false);\n\n /**\n * Horizontal page padding. `'default'`: 24/32/48/56/72/96 px from\n * 0/640/1024/1280/1440/1920 px. `'header'`: the same padding as `lc-header`\n * (16 px up to 768 px, 24 px above, from `--lc-density-padding-md`/`-lg`),\n * so the content lines up with the header title and the user menu. Combine\n * it with `contentMaxWidth=\"none\"` (or a page narrower than the cap).\n * `--lc-app-page-padding-x` wins over both.\n * @default 'default'\n */\n readonly paddingX = input<AppPagePaddingX>('default');\n\n /**\n * Space above the page content. `'default'`: 24/32/48/56/72/112 px by\n * width. `'none'` 0, `'sm'` 8 px, `'md'` 16 px at every width.\n * `--lc-app-page-padding-top` wins.\n * @default 'default'\n */\n readonly paddingTop = input<AppPagePaddingTop>('default');\n\n /**\n * Keeps the right column from 1280 px (360 · 400 · 460 px,\n * `--lc-app-page-aside-width`) with its tint and hairline but no content\n * and no landmark (`aria-hidden=\"true\"`, no role), so pages without\n * right-column content share one column with pages that have one. A named\n * aside (`asideLabelledby` / `asideAriaLabel`) wins; `contentWidth=\"wide\"`\n * leaves the column empty and untinted. Below 1280 px nothing shows.\n * @default false\n */\n readonly reserveAside = input(false);\n\n /**\n * `'wide'`, from 1280 px: the page keeps the standard column of a page with\n * a right column, the column stays empty and untinted, and `[page-wide]`\n * children run over it, up to 928 px (or `contentMaxWidth` when it is not\n * `880px`; `none` means the whole column). The other children keep the\n * standard column. With a named aside, `'wide'` has no effect. Below\n * 1280 px `'wide'` renders like `'default'`.\n * @default 'default'\n */\n readonly contentWidth = input<AppPageContentWidth>('default');\n\n /** The right column renders only when it has an accessible name. */\n protected readonly hasAside = computed(() => !!(this.asideLabelledby() || this.asideAriaLabel()));\n\n /** `contentWidth=\"wide\"` takes effect only without a named aside, which it yields to. */\n protected readonly wide = computed(() => this.contentWidth() === 'wide' && !this.hasAside());\n\n /** The empty, unnamed column (`reserveAside`), unless a named aside or `wide` takes the space. */\n protected readonly reserved = computed(() => this.reserveAside() && !this.hasAside() && !this.wide());\n\n /**\n * The cap of `[page-wide]` children: `null` keeps the CSS's 928 px (also\n * for the default `contentMaxWidth`), `none` means the whole column.\n */\n protected readonly wideMax = computed(() => {\n if (!this.wide()) return null;\n const max = this.contentMaxWidth();\n if (max === '880px') return null;\n return max === 'none' ? '100vw' : max;\n });\n\n protected readonly pageClasses = computed(() => {\n const classes = ['lc-app-page'];\n if (this.hasAside() || this.reserveAside() || this.wide()) classes.push('lc-app-page--aside');\n if (this.wide()) classes.push('lc-app-page--wide');\n if (this.paddingX() === 'header') classes.push('lc-app-page--pad-x-header');\n if (this.paddingTop() !== 'default') classes.push(`lc-app-page--pad-top-${this.paddingTop()}`);\n return classes.join(' ');\n });\n}\n","<div [class]=\"pageClasses()\" [style.--lc-app-page-content-max]=\"contentMaxWidth()\" [style.--_lc-app-page-wide-max]=\"wideMax()\">\n <main class=\"lc-app-page__main\" [id]=\"mainId()\" [attr.aria-busy]=\"busy() ? 'true' : null\">\n <div class=\"lc-app-page__content\"><ng-content /></div>\n </main>\n <!-- One outer block, so a page without a right column keeps 3.27.2's single\n anchor (an @else branch adds one); the reserved column (AS-35) is empty,\n aria-hidden and no landmark. -->\n @if (hasAside() || reserved()) {\n @if (hasAside()) {\n <aside\n class=\"lc-app-page__aside\"\n [attr.aria-labelledby]=\"asideLabelledby() || null\"\n [attr.aria-label]=\"asideLabelledby() ? null : asideAriaLabel()\"\n >\n <ng-content select=\"[page-aside]\" />\n </aside>\n } @else {<div class=\"lc-app-page__aside lc-app-page__aside--reserved\" aria-hidden=\"true\"></div>}\n }\n</div>\n"],"mappings":";;;AAiBA,MAAM,UAAU;AAGhB,MAAM,YAAY;AAClB,MAAM,eAAe;AAGrB,MAAM,mBAAmB,OAA4B,WACnD,MAAM,gBAAgB,EAAE,EAAE,aAAa,OAAO,sBAAqB,CAAE,CAAC;AAkGxE,IAAa,oBAAb,MAAa,kBAAiB;CAQnB,UAAU,MAAM,OAAM,GAAA,YAAA,CAAA,EAAA,WAAA,UAAA,CAAA,+BAAA,CAAA,CAAA;CAatB,eAAe,MAA4B,gBAAe,GAAA,YAAA,CAAA,EAAA,WAAA,eAAA,CAAA,+BAAA,CAAA,CAAA;CAEhD,eAAe,eAAc;EAC9C,MAAM,UAAU,CAAC,cAAc;EAC/B,IAAI,KAAK,QAAO,GAAI,QAAQ,KAAK,uBAAuB;EACxD,IAAI,KAAK,aAAY,MAAO,eAAe,QAAQ,KAAK,+BAA+B;EACvF,OAAO,QAAQ,KAAK,GAAG;CACzB,GAAE,GAAA,YAAA,CAAA,EAAA,WAAA,eAAA,CAAA,+BAAA,CAAA,CAAA;CAEe,QAAQ,UAAU,SAAkC,SAAQ,GAAA,YAAA,CAAA,EAAA,WAAA,QAAA,CAAA,+BAAA,CAAA,CAAA;CAC5D,aAAa,UAAU,SAAkC,cAAa,GAAA,YAAA,CAAA,EAAA,WAAA,aAAA,CAAA,+BAAA,CAAA,CAAA;CACtE,UAAU,UAAU,SAAkC,WAAU,GAAA,YAAA,CAAA,EAAA,WAAA,UAAA,CAAA,+BAAA,CAAA,CAAA;CAEjF,cAAA;EACE,MAAM,aAAa,OAAO,UAAU;EACpC,MAAM,SAAS,OAAO,oBAAoB;EAO1C,sBAAqB;GACnB,IAAI,OAAO,mBAAmB,aAAa;GAC3C,MAAM,QAAQ,KAAK,MAAK,CAAE,CAAC;GAC3B,MAAM,SAAS,KAAK,WAAU,CAAE,CAAC;GACjC,MAAM,OAAO,KAAK,QAAO,CAAE,CAAC;GAG5B,OAAO,IAAI,IAAI,SAAS;GACxB,OAAO,OAAO,IAAI,YAAY;GAC9B,IAAI,MAAsB;GAC1B,MAAM,cAAa;IACjB,MAAM,MAAM,eAAe,OAAO;IAClC,OAAO,OAAO,IAAI,YAAY;GAChC;GACA,MAAM,QAAQ,IAAI,gBAAgB,YAAW;IAC3C,MAAM,QAAQ,QAAQ,QAAQ,SAAS;IACvC,MAAM,SAAS,MAAM,iBAAiB,EAAE,EAAE,aAAa,MAAM,YAAY;IAEzE,IAAI,SAAS,KAAK,KAAK;KACrB,MAAM,MAAM,YAAY,SAAS,GAAG,OAAM,GAAI;KAG9C,MAAM,SAAS,WAAW,iBAAiB,GAAG,CAAC,CAAC,MAAM;KACtD,OAAO,OAAO,IAAI,IAAI,OAAO,SAAS,MAAM,IAAI,SAAS,KAAK,gBAAgB,OAAO,GAAG,EAAC,GAAI;IAC/F,OAAO,MAAK;GACd,CAAC;GAGD,MAAM,cAAc,IAAI,gBAAgB,YAAW;IACjD,MAAM,SAAS,gBAAgB,QAAQ,QAAQ,SAAS,IAAI,MAAM;IAClE,OAAO,IAAI,IAAI,SAAS,IAAI,GAAG,OAAM,MAAO,SAAS;GACvD,CAAC;GACD,YAAY,QAAQ,MAAM;GAE1B,MAAM,aAAY;IAChB,MAAM,OAAO,KAAK,cAAc,gBAAgB,KAAK,KAAK;IAC1D,IAAI,SAAS,KAAK;IAClB,IAAI,KAAK,MAAM,UAAU,GAAG;IAC5B,MAAM;IACN,IAAI,KAAK,MAAM,QAAQ,GAAG;SACrB,MAAK;GACZ;GACA,KAAI;GACJ,MAAM,UAAU,OAAO,qBAAqB,cAAc,OAAO,IAAI,iBAAiB,IAAI;GAC1F,SAAS,QAAQ,MAAM;IAAE,WAAW;IAAM,SAAS;GAAI,CAAE;GACzD,WAAW,gBAAe;IACxB,MAAM,WAAU;IAChB,YAAY,WAAU;IACtB,SAAS,WAAU;IACnB,OAAO,IAAI,IAAI,IAAI;IACnB,OAAO,OAAO,IAAI,IAAI;GACxB,CAAC;EACH,CAAC;CACH;CAhGW,OAAA,OAAA,GAAA,mBAAA;EAAA,YAAA;EAAA,SAAA;EAAA,UAAA;EAAA,MAAA;EAAiB,MAAA,CAAA;EAAA,QAAA,GAAA,gBAAA;CAAA,CAAA;CAAjB,OAAA,OAAA,GAAA,qBAAA;EAAA,YAAA;EAAA,SAAA;EAAA,MAAA;EAAiB,cAAA;EAAA,UAAA;EAAA,QAAA;GAAA,SAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;GAAA,cAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;EAAA;EAAA,aAAA;GAAA;IAAA,cAAA;IAAA,OAAA;IAAA,WAAA,CAAA,OAAA;IAAA,aAAA;IAAA,UAAA;GAAA;GAAA;IAAA,cAAA;IAAA,OAAA;IAAA,WAAA,CAAA,YAAA;IAAA,aAAA;IAAA,UAAA;GAAA;GAAA;IAAA,cAAA;IAAA,OAAA;IAAA,WAAA,CAAA,SAAA;IAAA,aAAA;IAAA,UAAA;GAAA;EAAA;EAAA,UAAA;EAAA,UC3H9B;EAUA,QAAA,CAAA,29HAAA;EAAA,iBAAA,GAAA,wBAAA;CAAA,CAAA;;ADiHa,GAAA,yBAAA;CAAA,YAAA;CAAA,SAAA;CAAA,UAAA;CAAA,MAAA;CAAiB,YAAA,CAAA;EAP7B,MAAA;EACW,MAAA,CAAA;GAAA,UAAA;GAAc,YACZ;GAAI,iBAGC,wBAAwB;GAAM,UAAA;GAAA,QAAA,CAAA,29HAAA;EAAA,CAAA;CAgCsB,CAAA;CAAA,sBAAA,CAAA;CAAA,gBAAA;EAAA,SAAA,CAAA;GAAA,MAAA,GAAA;GAAA,MAAA,CAAA;IAAA,UAAA;IAAA,OAAA;IAAA,UAAA;GAAA,CAAA;EAAA,CAAA;EAAA,cAAA,CAAA;GAAA,MAAA,GAAA;GAAA,MAAA,CAAA;IAAA,UAAA;IAAA,OAAA;IAAA,UAAA;GAAA,CAAA;EAAA,CAAA;EAAA,OAAA,CAAA;GAAA,MAAA,GAAA;GAAA,MAAA,CAAA,SAAO,EAAA,UAAA,KAAA,CAAA;EAAA,CAAA;EAAA,YAAA,CAAA;GAAA,MAAA,GAAA;GAAA,MAAA,CACF,cAAY,EAAA,UAAA,KAAA,CAAA;EAAA,CAAA;EAAA,SAAA,CAAA;GAAA,MAAA,GAAA;GAAA,MAAA,CACf,WAAS,EAAA,UAAA,KAAA,CAAA;EAAA,CAAA;CAAA;AAAA,CAAA;AEhElF,IAAa,mBAAb,MAAa,iBAAgB;CAElB,SAAS,MAAM,QAAO,GAAA,YAAA,CAAA,EAAA,WAAA,SAAA,CAAA,+BAAA,CAAA,CAAA;CAMtB,kBAAkB,MAAM,SAAQ,GAAA,YAAA,CAAA,EAAA,WAAA,kBAAA,CAAA,+BAAA,CAAA,CAAA;CAGhC,kBAAkB,MAAM,IAAG,GAAA,YAAA,CAAA,EAAA,WAAA,kBAAA,CAAA,+BAAA,CAAA,CAAA;CAG3B,iBAAiB,MAAM,IAAG,GAAA,YAAA,CAAA,EAAA,WAAA,iBAAA,CAAA,+BAAA,CAAA,CAAA;CAW1B,OAAO,MAAM,OAAM,GAAA,YAAA,CAAA,EAAA,WAAA,OAAA,CAAA,+BAAA,CAAA,CAAA;CAWnB,WAAW,MAAuB,WAAU,GAAA,YAAA,CAAA,EAAA,WAAA,WAAA,CAAA,+BAAA,CAAA,CAAA;CAQ5C,aAAa,MAAyB,WAAU,GAAA,YAAA,CAAA,EAAA,WAAA,aAAA,CAAA,+BAAA,CAAA,CAAA;CAWhD,eAAe,MAAM,OAAM,GAAA,YAAA,CAAA,EAAA,WAAA,eAAA,CAAA,+BAAA,CAAA,CAAA;CAW3B,eAAe,MAA2B,WAAU,GAAA,YAAA,CAAA,EAAA,WAAA,eAAA,CAAA,+BAAA,CAAA,CAAA;CAG1C,WAAW,eAAe,CAAC,EAAE,KAAK,gBAAe,KAAM,KAAK,eAAc,IAAI,GAAA,YAAA,CAAA,EAAA,WAAA,WAAA,CAAA,+BAAA,CAAA,CAAA;CAG9E,OAAO,eAAe,KAAK,aAAY,MAAO,UAAU,CAAC,KAAK,SAAQ,GAAG,GAAA,YAAA,CAAA,EAAA,WAAA,OAAA,CAAA,+BAAA,CAAA,CAAA;CAGzE,WAAW,eAAe,KAAK,aAAY,KAAM,CAAC,KAAK,SAAQ,KAAM,CAAC,KAAK,KAAI,GAAG,GAAA,YAAA,CAAA,EAAA,WAAA,WAAA,CAAA,+BAAA,CAAA,CAAA;CAMlF,UAAU,eAAc;EACzC,IAAI,CAAC,KAAK,KAAI,GAAI,OAAO;EACzB,MAAM,MAAM,KAAK,gBAAe;EAChC,IAAI,QAAQ,SAAS,OAAO;EAC5B,OAAO,QAAQ,SAAS,UAAU;CACpC,GAAE,GAAA,YAAA,CAAA,EAAA,WAAA,UAAA,CAAA,+BAAA,CAAA,CAAA;CAEiB,cAAc,eAAc;EAC7C,MAAM,UAAU,CAAC,aAAa;EAC9B,IAAI,KAAK,SAAQ,KAAM,KAAK,aAAY,KAAM,KAAK,KAAI,GAAI,QAAQ,KAAK,oBAAoB;EAC5F,IAAI,KAAK,KAAI,GAAI,QAAQ,KAAK,mBAAmB;EACjD,IAAI,KAAK,SAAQ,MAAO,UAAU,QAAQ,KAAK,2BAA2B;EAC1E,IAAI,KAAK,WAAU,MAAO,WAAW,QAAQ,KAAK,wBAAwB,KAAK,WAAU,GAAI;EAC7F,OAAO,QAAQ,KAAK,GAAG;CACzB,GAAE,GAAA,YAAA,CAAA,EAAA,WAAA,cAAA,CAAA,+BAAA,CAAA,CAAA;CA/FS,OAAA,OAAA,GAAA,mBAAA;EAAA,YAAA;EAAA,SAAA;EAAA,UAAA;EAAA,MAAA;EAAgB,MAAA,CAAA;EAAA,QAAA,GAAA,gBAAA;CAAA,CAAA;CAAhB,OAAA,OAAA,GAAA,qBAAA;EAAA,YAAA;EAAA,SAAA;EAAA,MAAA;EAAgB,cAAA;EAAA,UAAA;EAAA,QAAA;GAAA,QAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;GAAA,iBAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;GAAA,iBAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;GAAA,gBAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;GAAA,MAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;GAAA,UAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;GAAA,YAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;GAAA,cAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;GAAA,cAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;EAAA;EAAA,UAAA;EAAA,UC3F7B;EAmBA,QAAA,CAAA,orFAAA;EAAA,iBAAA,GAAA,wBAAA;CAAA,CAAA;;ADwEa,GAAA,yBAAA;CAAA,YAAA;CAAA,SAAA;CAAA,UAAA;CAAA,MAAA;CAAgB,YAAA,CAAA;EAP5B,MAAA;EACW,MAAA,CAAA;GAAA,UAAA;GAAa,YACX;GAAI,iBAGC,wBAAwB;GAAM,UAAA;GAAA,QAAA,CAAA,orFAAA;EAAA,CAAA"}
@@ -1,5 +1,5 @@
1
1
  import * as i0 from "@angular/core";
2
- import { Injectable } from "@angular/core";
2
+ import { Injectable, signal } from "@angular/core";
3
3
  var OverlayStackService = class OverlayStackService {
4
4
  stack = [];
5
5
  claimed = /* @__PURE__ */ new WeakSet();
@@ -45,6 +45,35 @@ i0.ɵɵngDeclareClassMetadata({
45
45
  args: [{ providedIn: "root" }]
46
46
  }]
47
47
  });
48
- export { OverlayStackService };
48
+ var OverlayInsetsService = class OverlayInsetsService {
49
+ top = signal(null, ...ngDevMode ? [{ debugName: "top" }] : /* istanbul ignore next */ []);
50
+ bottom = signal(null, ...ngDevMode ? [{ debugName: "bottom" }] : /* istanbul ignore next */ []);
51
+ static ɵfac = i0.ɵɵngDeclareFactory({
52
+ minVersion: "12.0.0",
53
+ version: "22.2.2",
54
+ ngImport: i0,
55
+ type: OverlayInsetsService,
56
+ deps: [],
57
+ target: i0.ɵɵFactoryTarget.Injectable
58
+ });
59
+ static ɵprov = i0.ɵɵngDeclareInjectable({
60
+ minVersion: "12.0.0",
61
+ version: "22.2.2",
62
+ ngImport: i0,
63
+ type: OverlayInsetsService,
64
+ providedIn: "root"
65
+ });
66
+ };
67
+ i0.ɵɵngDeclareClassMetadata({
68
+ minVersion: "12.0.0",
69
+ version: "22.2.2",
70
+ ngImport: i0,
71
+ type: OverlayInsetsService,
72
+ decorators: [{
73
+ type: Injectable,
74
+ args: [{ providedIn: "root" }]
75
+ }]
76
+ });
77
+ export { OverlayInsetsService, OverlayStackService };
49
78
 
50
79
  //# sourceMappingURL=lifecorner-angular-ui-kit-internal-overlay.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"lifecorner-angular-ui-kit-internal-overlay.mjs","names":[],"sources":["../../../../libs/angular-ui-kit/src/lib/internal/overlay/overlay-stack.service.ts"],"sourcesContent":["import { Injectable } from '@angular/core';\n\n/**\n * Keeps the open overlays (modal, drawer, popover, menu, tooltip, …) in\n * opening order so that a dismiss gesture — Escape, click outside — reaches\n * only the top-most one instead of closing every open layer at once.\n *\n * Every overlay pushes its id when it opens and removes it when it closes or\n * is destroyed. Internal to the library; not part of the public API.\n */\n@Injectable({ providedIn: 'root' })\nexport class OverlayStackService {\n private readonly stack: string[] = [];\n\n /**\n * Events already consumed by an overlay. All overlays listen on `document`,\n * where listeners fire in registration order — not z-order — so by the time a\n * lower overlay's listener runs, the top one may already have closed and left\n * the stack, making the lower one \"top\" for the very same event.\n */\n private readonly claimed = new WeakSet<Event>();\n\n /** Register `id` as the new top-most overlay (re-pushing moves it to the top). */\n push(id: string): void {\n this.remove(id);\n this.stack.push(id);\n }\n\n remove(id: string): void {\n const index = this.stack.indexOf(id);\n if (index !== -1) this.stack.splice(index, 1);\n }\n\n isTop(id: string): boolean {\n return this.stack.length > 0 && this.stack[this.stack.length - 1] === id;\n }\n\n /**\n * Whether overlay `id` may act on this dismiss event. True only for the\n * top-most overlay and only once per event; the caller then closes and, for\n * keyboard events, stops propagation so nothing outside the stack sees it.\n */\n claim(id: string, event: Event): boolean {\n if (this.claimed.has(event) || !this.isTop(id)) return false;\n this.claimed.add(event);\n return true;\n }\n}\n"],"mappings":";;AAWA,IAAa,sBAAb,MAAa,oBAAmB;CACb,QAAkB,CAAA;CAQlB,0BAAU,IAAI,QAAO;CAGtC,KAAK,IAAU;EACb,KAAK,OAAO,EAAE;EACd,KAAK,MAAM,KAAK,EAAE;CACpB;CAEA,OAAO,IAAU;EACf,MAAM,QAAQ,KAAK,MAAM,QAAQ,EAAE;EACnC,IAAI,UAAU,IAAI,KAAK,MAAM,OAAO,OAAO,CAAC;CAC9C;CAEA,MAAM,IAAU;EACd,OAAO,KAAK,MAAM,SAAS,KAAK,KAAK,MAAM,KAAK,MAAM,SAAS,OAAO;CACxE;CAOA,MAAM,IAAY,OAAY;EAC5B,IAAI,KAAK,QAAQ,IAAI,KAAK,KAAK,CAAC,KAAK,MAAM,EAAE,GAAG,OAAO;EACvD,KAAK,QAAQ,IAAI,KAAK;EACtB,OAAO;CACT;CAnCW,OAAA,OAAA,GAAA,mBAAA;EAAA,YAAA;EAAA,SAAA;EAAA,UAAA;EAAA,MAAA;EAAmB,MAAA,CAAA;EAAA,QAAA,GAAA,gBAAA;CAAA,CAAA;CAAnB,OAAA,QAAA,GAAA,sBAAA;EAAA,YAAA;EAAA,SAAA;EAAA,UAAA;EAAA,MAAA;EAAmB,YADN;CAAM,CAAA;;AACnB,GAAA,yBAAA;CAAA,YAAA;CAAA,SAAA;CAAA,UAAA;CAAA,MAAA;CAAmB,YAAA,CAAA;EAD/B,MAAA;EAAW,MAAA,CAAA,EAAE,YAAY,OAAM,CAAE"}
1
+ {"version":3,"file":"lifecorner-angular-ui-kit-internal-overlay.mjs","names":[],"sources":["../../../../libs/angular-ui-kit/src/lib/internal/overlay/overlay-stack.service.ts","../../../../libs/angular-ui-kit/src/lib/internal/overlay/overlay-insets.service.ts"],"sourcesContent":["import { Injectable } from '@angular/core';\n\n/**\n * Keeps the open overlays (modal, drawer, popover, menu, tooltip, …) in\n * opening order so that a dismiss gesture — Escape, click outside — reaches\n * only the top-most one instead of closing every open layer at once.\n *\n * Every overlay pushes its id when it opens and removes it when it closes or\n * is destroyed. Internal to the library; not part of the public API.\n */\n@Injectable({ providedIn: 'root' })\nexport class OverlayStackService {\n private readonly stack: string[] = [];\n\n /**\n * Events already consumed by an overlay. All overlays listen on `document`,\n * where listeners fire in registration order — not z-order — so by the time a\n * lower overlay's listener runs, the top one may already have closed and left\n * the stack, making the lower one \"top\" for the very same event.\n */\n private readonly claimed = new WeakSet<Event>();\n\n /** Register `id` as the new top-most overlay (re-pushing moves it to the top). */\n push(id: string): void {\n this.remove(id);\n this.stack.push(id);\n }\n\n remove(id: string): void {\n const index = this.stack.indexOf(id);\n if (index !== -1) this.stack.splice(index, 1);\n }\n\n isTop(id: string): boolean {\n return this.stack.length > 0 && this.stack[this.stack.length - 1] === id;\n }\n\n /**\n * Whether overlay `id` may act on this dismiss event. True only for the\n * top-most overlay and only once per event; the caller then closes and, for\n * keyboard events, stops propagation so nothing outside the stack sees it.\n */\n claim(id: string, event: Event): boolean {\n if (this.claimed.has(event) || !this.isTop(id)) return false;\n this.claimed.add(event);\n return true;\n }\n}\n","import { Injectable, signal } from '@angular/core';\n\n/**\n * The space a fixed overlay keeps free at the top and the bottom of the\n * viewport, as CSS lengths (3.28, AS-34). `lc-app-shell` writes them in the\n * browser: `top` is its header's bottom edge, `bottom` the shown bottom bar's\n * top edge, measured from the viewport's bottom (or the safe-area insets when\n * there is no header or no bar). `lc-toast-outlet` reads them, so its stacks\n * clear the header and the bar even when the app renders the outlet outside\n * the shell (no custom property can inherit there).\n *\n * `null` means \"no shell\": on the server, before the shell has measured,\n * without ResizeObserver and after the shell is destroyed. Internal to the\n * library; not part of the public API and without a semver guarantee.\n */\n@Injectable({ providedIn: 'root' })\nexport class OverlayInsetsService {\n /** CSS length kept free above top-edge overlays, e.g. `'56px'`. */\n readonly top = signal<string | null>(null);\n\n /** CSS length kept free below bottom-edge overlays, e.g. `'102px'`. */\n readonly bottom = signal<string | null>(null);\n}\n"],"mappings":";;AAWA,IAAa,sBAAb,MAAa,oBAAmB;CACb,QAAkB,CAAA;CAQlB,0BAAU,IAAI,QAAO;CAGtC,KAAK,IAAU;EACb,KAAK,OAAO,EAAE;EACd,KAAK,MAAM,KAAK,EAAE;CACpB;CAEA,OAAO,IAAU;EACf,MAAM,QAAQ,KAAK,MAAM,QAAQ,EAAE;EACnC,IAAI,UAAU,IAAI,KAAK,MAAM,OAAO,OAAO,CAAC;CAC9C;CAEA,MAAM,IAAU;EACd,OAAO,KAAK,MAAM,SAAS,KAAK,KAAK,MAAM,KAAK,MAAM,SAAS,OAAO;CACxE;CAOA,MAAM,IAAY,OAAY;EAC5B,IAAI,KAAK,QAAQ,IAAI,KAAK,KAAK,CAAC,KAAK,MAAM,EAAE,GAAG,OAAO;EACvD,KAAK,QAAQ,IAAI,KAAK;EACtB,OAAO;CACT;CAnCW,OAAA,OAAA,GAAA,mBAAA;EAAA,YAAA;EAAA,SAAA;EAAA,UAAA;EAAA,MAAA;EAAmB,MAAA,CAAA;EAAA,QAAA,GAAA,gBAAA;CAAA,CAAA;CAAnB,OAAA,QAAA,GAAA,sBAAA;EAAA,YAAA;EAAA,SAAA;EAAA,UAAA;EAAA,MAAA;EAAmB,YADN;CAAM,CAAA;;AACnB,GAAA,yBAAA;CAAA,YAAA;CAAA,SAAA;CAAA,UAAA;CAAA,MAAA;CAAmB,YAAA,CAAA;EAD/B,MAAA;EAAW,MAAA,CAAA,EAAE,YAAY,OAAM,CAAE;;;ACMlC,IAAa,uBAAb,MAAa,qBAAoB;CAEtB,MAAM,OAAsB,MAAK,GAAA,YAAA,CAAA,EAAA,WAAA,MAAA,CAAA,+BAAA,CAAA,CAAA;CAGjC,SAAS,OAAsB,MAAK,GAAA,YAAA,CAAA,EAAA,WAAA,SAAA,CAAA,+BAAA,CAAA,CAAA;CALlC,OAAA,OAAA,GAAA,mBAAA;EAAA,YAAA;EAAA,SAAA;EAAA,UAAA;EAAA,MAAA;EAAoB,MAAA,CAAA;EAAA,QAAA,GAAA,gBAAA;CAAA,CAAA;CAApB,OAAA,QAAA,GAAA,sBAAA;EAAA,YAAA;EAAA,SAAA;EAAA,UAAA;EAAA,MAAA;EAAoB,YADP;CAAM,CAAA;;AACnB,GAAA,yBAAA;CAAA,YAAA;CAAA,SAAA;CAAA,UAAA;CAAA,MAAA;CAAoB,YAAA,CAAA;EADhC,MAAA;EAAW,MAAA,CAAA,EAAE,YAAY,OAAM,CAAE"}
@@ -3,6 +3,7 @@ import { ChangeDetectionStrategy, Component, Injectable, PLATFORM_ID, ViewEncaps
3
3
  import { isPlatformBrowser } from "@angular/common";
4
4
  import { IconComponent, ɵprovideKitIcons } from "@lifecorner/angular-ui-kit/icon";
5
5
  import { ICON_CHECK_CIRCLE, ICON_CIRCLE_X, ICON_EXCLAMATION_TRIANGLE, ICON_INFORMATION_CIRCLE, ICON_X_MARK } from "@lifecorner/angular-ui-kit/icons";
6
+ import { OverlayInsetsService } from "@lifecorner/angular-ui-kit/internal/overlay";
6
7
  var ToastService = class ToastService {
7
8
  toasts;
8
9
  toastList = signal([], ...ngDevMode ? [{ debugName: "toastList" }] : /* istanbul ignore next */ []);
@@ -279,6 +280,7 @@ var ToastOutletComponent = class ToastOutletComponent {
279
280
  positions = POSITIONS;
280
281
  liveLevels = ["assertive", "polite"];
281
282
  toastService = inject(ToastService);
283
+ insets = inject(OverlayInsetsService);
282
284
  closeAriaLabel = input("Close notification", ...ngDevMode ? [{ debugName: "closeAriaLabel" }] : /* istanbul ignore next */ []);
283
285
  groups = computed(() => {
284
286
  const groups = /* @__PURE__ */ new Map();
@@ -320,9 +322,13 @@ var ToastOutletComponent = class ToastOutletComponent {
320
322
  isRequired: false,
321
323
  transformFunction: null
322
324
  } },
325
+ host: { properties: {
326
+ "style.--_lc-toast-outlet-frame-top": "insets.top()",
327
+ "style.--_lc-toast-outlet-frame-bottom": "insets.bottom()"
328
+ } },
323
329
  ngImport: i0,
324
330
  template: "@for (position of positions; track position) {\n <div class=\"lc-toast-outlet__stack\" [class]=\"'lc-toast-outlet__stack lc-toast-outlet__stack--' + position\">\n <!-- Both regions are always present (even empty) so that toasts inserted\n later are announced; the toasts themselves do not announce again. -->\n @for (live of liveLevels; track live) {\n <div class=\"lc-toast-outlet__region\" [attr.aria-live]=\"live\" aria-atomic=\"false\">\n @for (toast of toastsFor(position, live); track toast.id) {\n <lc-toast [toast]=\"toast\" [announce]=\"false\" [closeAriaLabel]=\"closeAriaLabel()\" (closed)=\"onClosed($event)\" />\n }\n </div>\n }\n </div>\n}\n",
325
- styles: [":host{display:contents}.lc-toast-outlet__stack{position:fixed;z-index:1200;display:flex;flex-direction:column;width:100%;max-width:calc(400px + 2 * var(--spacing-4));padding:var(--spacing-4);box-sizing:border-box;pointer-events:none}.lc-toast-outlet__stack--top-left,.lc-toast-outlet__stack--top-center,.lc-toast-outlet__stack--top-right{top:0}.lc-toast-outlet__stack--bottom-left,.lc-toast-outlet__stack--bottom-center,.lc-toast-outlet__stack--bottom-right{bottom:0;flex-direction:column-reverse}.lc-toast-outlet__stack--top-left,.lc-toast-outlet__stack--bottom-left{left:0;align-items:flex-start}.lc-toast-outlet__stack--top-right,.lc-toast-outlet__stack--bottom-right{right:0;align-items:flex-end}.lc-toast-outlet__stack--top-center,.lc-toast-outlet__stack--bottom-center{left:50%;transform:translate(-50%);align-items:center}.lc-toast-outlet__region{display:flex;flex-direction:inherit;align-items:inherit;width:100%}\n"],
331
+ styles: [":host{display:contents}@property --_lc-toast-outlet-top{syntax: \"<length-percentage>\"; inherits: false; initial-value: 0px;}@property --_lc-toast-outlet-bottom{syntax: \"<length-percentage>\"; inherits: false; initial-value: 0px;}.lc-toast-outlet__stack{--_lc-toast-outlet-top: var(--lc-toast-outlet-offset-top, var(--_lc-toast-outlet-frame-top, 0px));--_lc-toast-outlet-bottom: var(--lc-toast-outlet-offset-bottom, var(--_lc-toast-outlet-frame-bottom, 0px));position:fixed;z-index:1200;display:flex;flex-direction:column;width:100%;max-width:calc(400px + 2 * var(--spacing-4));padding:var(--spacing-4);box-sizing:border-box;pointer-events:none;max-height:calc(100vh - var(--_lc-toast-outlet-top) - var(--_lc-toast-outlet-bottom));max-height:calc(100dvh - var(--_lc-toast-outlet-top) - var(--_lc-toast-outlet-bottom));overflow-y:auto;overscroll-behavior:contain}.lc-toast-outlet__stack--top-left,.lc-toast-outlet__stack--top-center,.lc-toast-outlet__stack--top-right{top:var(--_lc-toast-outlet-top)}.lc-toast-outlet__stack--bottom-left,.lc-toast-outlet__stack--bottom-center,.lc-toast-outlet__stack--bottom-right{bottom:var(--_lc-toast-outlet-bottom);flex-direction:column-reverse}.lc-toast-outlet__stack--top-left,.lc-toast-outlet__stack--bottom-left{left:0;align-items:flex-start}.lc-toast-outlet__stack--top-right,.lc-toast-outlet__stack--bottom-right{right:0;align-items:flex-end}.lc-toast-outlet__stack--top-center,.lc-toast-outlet__stack--bottom-center{left:50%;transform:translate(-50%);align-items:center}.lc-toast-outlet__region{display:flex;flex-direction:inherit;align-items:inherit;width:100%}\n"],
326
332
  dependencies: [{
327
333
  kind: "component",
328
334
  type: ToastComponent,
@@ -349,8 +355,12 @@ i0.ɵɵngDeclareClassMetadata({
349
355
  standalone: true,
350
356
  imports: [ToastComponent],
351
357
  changeDetection: ChangeDetectionStrategy.OnPush,
358
+ host: {
359
+ "[style.--_lc-toast-outlet-frame-top]": "insets.top()",
360
+ "[style.--_lc-toast-outlet-frame-bottom]": "insets.bottom()"
361
+ },
352
362
  template: "@for (position of positions; track position) {\n <div class=\"lc-toast-outlet__stack\" [class]=\"'lc-toast-outlet__stack lc-toast-outlet__stack--' + position\">\n <!-- Both regions are always present (even empty) so that toasts inserted\n later are announced; the toasts themselves do not announce again. -->\n @for (live of liveLevels; track live) {\n <div class=\"lc-toast-outlet__region\" [attr.aria-live]=\"live\" aria-atomic=\"false\">\n @for (toast of toastsFor(position, live); track toast.id) {\n <lc-toast [toast]=\"toast\" [announce]=\"false\" [closeAriaLabel]=\"closeAriaLabel()\" (closed)=\"onClosed($event)\" />\n }\n </div>\n }\n </div>\n}\n",
353
- styles: [":host{display:contents}.lc-toast-outlet__stack{position:fixed;z-index:1200;display:flex;flex-direction:column;width:100%;max-width:calc(400px + 2 * var(--spacing-4));padding:var(--spacing-4);box-sizing:border-box;pointer-events:none}.lc-toast-outlet__stack--top-left,.lc-toast-outlet__stack--top-center,.lc-toast-outlet__stack--top-right{top:0}.lc-toast-outlet__stack--bottom-left,.lc-toast-outlet__stack--bottom-center,.lc-toast-outlet__stack--bottom-right{bottom:0;flex-direction:column-reverse}.lc-toast-outlet__stack--top-left,.lc-toast-outlet__stack--bottom-left{left:0;align-items:flex-start}.lc-toast-outlet__stack--top-right,.lc-toast-outlet__stack--bottom-right{right:0;align-items:flex-end}.lc-toast-outlet__stack--top-center,.lc-toast-outlet__stack--bottom-center{left:50%;transform:translate(-50%);align-items:center}.lc-toast-outlet__region{display:flex;flex-direction:inherit;align-items:inherit;width:100%}\n"]
363
+ styles: [":host{display:contents}@property --_lc-toast-outlet-top{syntax: \"<length-percentage>\"; inherits: false; initial-value: 0px;}@property --_lc-toast-outlet-bottom{syntax: \"<length-percentage>\"; inherits: false; initial-value: 0px;}.lc-toast-outlet__stack{--_lc-toast-outlet-top: var(--lc-toast-outlet-offset-top, var(--_lc-toast-outlet-frame-top, 0px));--_lc-toast-outlet-bottom: var(--lc-toast-outlet-offset-bottom, var(--_lc-toast-outlet-frame-bottom, 0px));position:fixed;z-index:1200;display:flex;flex-direction:column;width:100%;max-width:calc(400px + 2 * var(--spacing-4));padding:var(--spacing-4);box-sizing:border-box;pointer-events:none;max-height:calc(100vh - var(--_lc-toast-outlet-top) - var(--_lc-toast-outlet-bottom));max-height:calc(100dvh - var(--_lc-toast-outlet-top) - var(--_lc-toast-outlet-bottom));overflow-y:auto;overscroll-behavior:contain}.lc-toast-outlet__stack--top-left,.lc-toast-outlet__stack--top-center,.lc-toast-outlet__stack--top-right{top:var(--_lc-toast-outlet-top)}.lc-toast-outlet__stack--bottom-left,.lc-toast-outlet__stack--bottom-center,.lc-toast-outlet__stack--bottom-right{bottom:var(--_lc-toast-outlet-bottom);flex-direction:column-reverse}.lc-toast-outlet__stack--top-left,.lc-toast-outlet__stack--bottom-left{left:0;align-items:flex-start}.lc-toast-outlet__stack--top-right,.lc-toast-outlet__stack--bottom-right{right:0;align-items:flex-end}.lc-toast-outlet__stack--top-center,.lc-toast-outlet__stack--bottom-center{left:50%;transform:translate(-50%);align-items:center}.lc-toast-outlet__region{display:flex;flex-direction:inherit;align-items:inherit;width:100%}\n"]
354
364
  }]
355
365
  }],
356
366
  propDecorators: { closeAriaLabel: [{
@@ -1 +1 @@
1
- {"version":3,"file":"lifecorner-angular-ui-kit-toast.mjs","names":[],"sources":["../../../../libs/angular-ui-kit/src/lib/toast/toast.service.ts","../../../../libs/angular-ui-kit/src/lib/toast/toast.component.ts","../../../../libs/angular-ui-kit/src/lib/toast/toast.component.html","../../../../libs/angular-ui-kit/src/lib/toast/toast-outlet.component.ts","../../../../libs/angular-ui-kit/src/lib/toast/toast-outlet.component.html"],"sourcesContent":["import { Injectable, PLATFORM_ID, inject, signal, Signal } from '@angular/core';\nimport { isPlatformBrowser } from '@angular/common';\n\nexport type ToastVariant = 'success' | 'error' | 'warning' | 'info';\nexport type ToastPosition =\n | 'top-left'\n | 'top-center'\n | 'top-right'\n | 'bottom-left'\n | 'bottom-center'\n | 'bottom-right';\n\nexport interface ToastAction {\n label: string;\n onClick: () => void;\n}\n\nexport interface ToastConfig {\n message: string;\n variant?: ToastVariant;\n position?: ToastPosition;\n duration?: number;\n showCloseButton?: boolean;\n action?: ToastAction;\n}\n\nexport interface Toast extends Required<Omit<ToastConfig, 'action'>> {\n id: string;\n action?: ToastAction;\n}\n\n/**\n * Toast service for showing ephemeral notifications.\n *\n * The toasts are rendered by `<lc-toast-outlet />`, which the application\n * places once (e.g. in its shell); without it nothing is shown. The options,\n * their defaults, the timing and how a toast is announced to screen readers\n * are documented under *Feedback/Toast Outlet* (3.17).\n *\n * @example\n * ```typescript\n * private readonly toastService = inject(ToastService);\n *\n * showSuccess() {\n * this.toastService.show({\n * message: 'Changes saved successfully!',\n * variant: 'success',\n * duration: 3000\n * });\n * }\n * ```\n */\n@Injectable({\n providedIn: 'root',\n})\nexport class ToastService {\n /**\n * Public readonly signal for toasts\n */\n public readonly toasts: Signal<readonly Toast[]>;\n\n /**\n * Internal signal for toast list\n */\n private readonly toastList = signal<Toast[]>([]);\n\n private readonly MAX_TOASTS = 5;\n private toastIdCounter = 0;\n private activeTimeouts = new Map<string, ReturnType<typeof setTimeout>>();\n /** Auto-dismiss timers only make sense in the browser (and `window` does not exist on the server). */\n private readonly isBrowser = isPlatformBrowser(inject(PLATFORM_ID));\n\n constructor() {\n this.toasts = this.toastList.asReadonly();\n }\n\n /**\n * Show a toast notification\n */\n show(config: ToastConfig): string {\n const toast: Toast = {\n id: this.generateId(),\n message: config.message,\n variant: config.variant || 'info',\n position: config.position || 'top-right',\n duration: config.duration !== undefined ? config.duration : 3000,\n showCloseButton: config.showCloseButton !== undefined ? config.showCloseButton : true,\n action: config.action,\n };\n\n // Add toast to list\n let currentToasts = this.toastList();\n\n // Remove oldest toast if at max capacity\n if (currentToasts.length >= this.MAX_TOASTS) {\n const oldestToast = currentToasts[0];\n if (oldestToast) {\n this.clearTimeout(oldestToast.id);\n currentToasts = currentToasts.slice(1);\n }\n }\n\n this.toastList.set([...currentToasts, toast]);\n\n // Auto-dismiss if duration > 0\n if (toast.duration > 0 && this.isBrowser) {\n const timeoutId = setTimeout(() => {\n this.close(toast.id);\n }, toast.duration);\n\n this.activeTimeouts.set(toast.id, timeoutId);\n }\n\n return toast.id;\n }\n\n /**\n * Close a specific toast\n */\n close(id: string): void {\n this.removeToast(id);\n }\n\n /**\n * Close all toasts\n */\n closeAll(): void {\n const currentToasts = this.toastList();\n currentToasts.forEach((toast) => {\n this.clearTimeout(toast.id);\n });\n this.toastList.set([]);\n }\n\n /**\n * Generate unique toast ID\n */\n private generateId(): string {\n return `toast-${++this.toastIdCounter}-${Date.now()}`;\n }\n\n /**\n * Remove a toast from the list\n */\n private removeToast(id: string): void {\n this.clearTimeout(id);\n const currentToasts = this.toastList();\n this.toastList.set(currentToasts.filter((t) => t.id !== id));\n }\n\n /**\n * Clear timeout for a toast\n */\n private clearTimeout(id: string): void {\n const timeoutId = this.activeTimeouts.get(id);\n if (timeoutId !== undefined) {\n clearTimeout(timeoutId);\n this.activeTimeouts.delete(id);\n }\n }\n}\n","import {\n Component,\n ChangeDetectionStrategy,\n input,\n output,\n computed,\n ViewEncapsulation,\n} from '@angular/core';\nimport { IconComponent, ɵprovideKitIcons } from '@lifecorner/angular-ui-kit/icon';\nimport {\n ICON_CHECK_CIRCLE,\n ICON_CIRCLE_X,\n ICON_EXCLAMATION_TRIANGLE,\n ICON_INFORMATION_CIRCLE,\n ICON_X_MARK,\n} from '@lifecorner/angular-ui-kit/icons';\nimport type { Toast, ToastVariant } from './toast.service';\n\n/**\n * Toast component for displaying ephemeral notification messages.\n *\n * Features:\n * - Semantic variants (success, warning, error, info)\n * - Auto-dismiss with configurable duration\n * - Manual dismiss with close button\n * - Icon display per variant\n * - Stacked toast positioning via ToastService + `<lc-toast-outlet />`\n * - Animated enter/exit transitions\n *\n * Usually rendered by `<lc-toast-outlet />`; can also be placed directly:\n *\n * @example\n * ```html\n * <lc-toast [toast]=\"toast\" (closed)=\"dismiss($event)\" />\n * ```\n */\n@Component({\n selector: 'lc-toast',\n standalone: true,\n imports: [IconComponent],\n viewProviders: [\n ɵprovideKitIcons([ICON_X_MARK, ICON_CHECK_CIRCLE, ICON_CIRCLE_X, ICON_EXCLAMATION_TRIANGLE, ICON_INFORMATION_CIRCLE]),\n ],\n templateUrl: './toast.component.html',\n styleUrls: ['./toast.component.scss'],\n encapsulation: ViewEncapsulation.None, // Required for dynamic variant class styling\n changeDetection: ChangeDetectionStrategy.OnPush,\n host: {\n '[class]': 'hostClasses()',\n '[attr.role]': 'ariaRole()',\n '[attr.aria-live]': 'ariaLive()',\n '[attr.aria-atomic]': 'announce() ? \"true\" : null',\n },\n})\nexport class ToastComponent {\n /**\n * Toast data\n */\n toast = input.required<Toast>();\n\n /**\n * Whether the toast is its own live region (`role=\"status\"`/`\"alert\"` on the\n * host). `lc-toast-outlet` sets this to false because its persistent live\n * regions already announce the toast — announcing twice would be noise.\n * @default true\n */\n announce = input<boolean>(true);\n\n /** Accessible name of the close button (`toast.showCloseButton`). */\n closeAriaLabel = input('Close notification');\n\n /**\n * Emitted when toast is closed\n */\n readonly closed = output<string>();\n\n /**\n * Computed host classes\n */\n protected hostClasses = computed(() => {\n const toast = this.toast();\n return `lc-toast lc-toast--${toast.variant}`;\n });\n\n /**\n * Computed container classes\n */\n protected containerClasses = computed(() => {\n return 'lc-toast__container';\n });\n\n /**\n * Computed ARIA role\n */\n protected ariaRole = computed(() => {\n if (!this.announce()) return null;\n const variant = this.toast().variant;\n return variant === 'error' || variant === 'warning' ? 'alert' : 'status';\n });\n\n /**\n * Computed ARIA live region priority\n */\n protected ariaLive = computed(() => {\n if (!this.announce()) return null;\n const variant = this.toast().variant;\n return variant === 'error' || variant === 'warning' ? 'assertive' : 'polite';\n });\n\n /**\n * Computed default icon based on variant\n */\n protected defaultIcon = computed(() => {\n const icons: Record<ToastVariant, string> = {\n success: 'check-circle',\n error: 'x-circle',\n warning: 'exclamation-triangle',\n info: 'information-circle',\n };\n return icons[this.toast().variant];\n });\n\n /**\n * Handle close button click\n */\n protected onCloseClick(event: Event): void {\n event.preventDefault();\n event.stopPropagation();\n this.closed.emit(this.toast().id);\n }\n\n /**\n * Handle action button click\n */\n protected onActionClick(event: Event): void {\n event.preventDefault();\n event.stopPropagation();\n const action = this.toast().action;\n if (action) {\n action.onClick();\n this.closed.emit(this.toast().id);\n }\n }\n}\n","<div [class]=\"containerClasses()\">\n <div class=\"lc-toast__icon\">\n <lc-icon [name]=\"defaultIcon()\" size=\"md\" [decorative]=\"true\" />\n </div>\n \n <div class=\"lc-toast__message\">\n {{ toast().message }}\n </div>\n \n @if (toast().action) {\n <button\n type=\"button\"\n class=\"lc-toast__action\"\n (click)=\"onActionClick($event)\"\n >\n {{ toast().action?.label }}\n </button>\n }\n \n @if (toast().showCloseButton) {\n <button\n type=\"button\"\n class=\"lc-toast__close\"\n [attr.aria-label]=\"closeAriaLabel()\"\n (click)=\"onCloseClick($event)\"\n >\n <lc-icon name=\"x-mark\" size=\"sm\" [decorative]=\"true\" />\n </button>\n }\n</div>\n","import { Component, ChangeDetectionStrategy, computed, inject, input } from '@angular/core';\nimport { ToastComponent } from './toast.component';\nimport { ToastService, type Toast, type ToastPosition } from './toast.service';\n\n/** Politeness of the live region a toast is announced from. */\ntype ToastLive = 'polite' | 'assertive';\n\nconst POSITIONS: readonly ToastPosition[] = [\n 'top-left',\n 'top-center',\n 'top-right',\n 'bottom-left',\n 'bottom-center',\n 'bottom-right',\n];\n\n/**\n * Renders the toasts of {@link ToastService}. Place it once in the application\n * shell; without it `ToastService.show()` has no visible effect.\n *\n * ```html\n * <lc-toast-outlet />\n * ```\n *\n * Toasts are stacked per `position`. Every position owns two live regions that\n * exist from the start — screen readers only announce content that is inserted\n * into an *existing* live region — one `polite` for success/info toasts and one\n * `assertive` for error/warning toasts. Dismissing a toast (close button,\n * action, auto-dismiss) removes it from the service.\n */\n@Component({\n selector: 'lc-toast-outlet',\n standalone: true,\n imports: [ToastComponent],\n templateUrl: './toast-outlet.component.html',\n styleUrls: ['./toast-outlet.component.scss'],\n changeDetection: ChangeDetectionStrategy.OnPush,\n})\nexport class ToastOutletComponent {\n protected readonly positions = POSITIONS;\n protected readonly liveLevels: readonly ToastLive[] = ['assertive', 'polite'];\n\n private readonly toastService = inject(ToastService);\n\n /** Accessible name of every toast's close button (forwarded to lc-toast). */\n readonly closeAriaLabel = input('Close notification');\n\n /** Toasts keyed by `${position}:${politeness}` in the order they were shown. */\n private readonly groups = computed(() => {\n const groups = new Map<string, Toast[]>();\n for (const toast of this.toastService.toasts()) {\n const key = `${toast.position}:${ToastOutletComponent.liveFor(toast)}`;\n const group = groups.get(key);\n if (group) {\n group.push(toast);\n } else {\n groups.set(key, [toast]);\n }\n }\n return groups;\n });\n\n protected toastsFor(position: ToastPosition, live: ToastLive): readonly Toast[] {\n return this.groups().get(`${position}:${live}`) ?? [];\n }\n\n protected onClosed(id: string): void {\n this.toastService.close(id);\n }\n\n /** Errors and warnings interrupt (assertive), everything else waits its turn. */\n private static liveFor(toast: Toast): ToastLive {\n return toast.variant === 'error' || toast.variant === 'warning' ? 'assertive' : 'polite';\n }\n}\n","@for (position of positions; track position) {\n <div class=\"lc-toast-outlet__stack\" [class]=\"'lc-toast-outlet__stack lc-toast-outlet__stack--' + position\">\n <!-- Both regions are always present (even empty) so that toasts inserted\n later are announced; the toasts themselves do not announce again. -->\n @for (live of liveLevels; track live) {\n <div class=\"lc-toast-outlet__region\" [attr.aria-live]=\"live\" aria-atomic=\"false\">\n @for (toast of toastsFor(position, live); track toast.id) {\n <lc-toast [toast]=\"toast\" [announce]=\"false\" [closeAriaLabel]=\"closeAriaLabel()\" (closed)=\"onClosed($event)\" />\n }\n </div>\n }\n </div>\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"lifecorner-angular-ui-kit-toast.mjs","names":[],"sources":["../../../../libs/angular-ui-kit/src/lib/toast/toast.service.ts","../../../../libs/angular-ui-kit/src/lib/toast/toast.component.ts","../../../../libs/angular-ui-kit/src/lib/toast/toast.component.html","../../../../libs/angular-ui-kit/src/lib/toast/toast-outlet.component.ts","../../../../libs/angular-ui-kit/src/lib/toast/toast-outlet.component.html"],"sourcesContent":["import { Injectable, PLATFORM_ID, inject, signal, Signal } from '@angular/core';\nimport { isPlatformBrowser } from '@angular/common';\n\nexport type ToastVariant = 'success' | 'error' | 'warning' | 'info';\nexport type ToastPosition =\n | 'top-left'\n | 'top-center'\n | 'top-right'\n | 'bottom-left'\n | 'bottom-center'\n | 'bottom-right';\n\nexport interface ToastAction {\n label: string;\n onClick: () => void;\n}\n\nexport interface ToastConfig {\n message: string;\n variant?: ToastVariant;\n position?: ToastPosition;\n duration?: number;\n showCloseButton?: boolean;\n action?: ToastAction;\n}\n\nexport interface Toast extends Required<Omit<ToastConfig, 'action'>> {\n id: string;\n action?: ToastAction;\n}\n\n/**\n * Toast service for showing ephemeral notifications.\n *\n * The toasts are rendered by `<lc-toast-outlet />`, which the application\n * places once (e.g. in its shell); without it nothing is shown. The options,\n * their defaults, the timing and how a toast is announced to screen readers\n * are documented under *Feedback/Toast Outlet* (3.17).\n *\n * @example\n * ```typescript\n * private readonly toastService = inject(ToastService);\n *\n * showSuccess() {\n * this.toastService.show({\n * message: 'Changes saved successfully!',\n * variant: 'success',\n * duration: 3000\n * });\n * }\n * ```\n */\n@Injectable({\n providedIn: 'root',\n})\nexport class ToastService {\n /**\n * Public readonly signal for toasts\n */\n public readonly toasts: Signal<readonly Toast[]>;\n\n /**\n * Internal signal for toast list\n */\n private readonly toastList = signal<Toast[]>([]);\n\n private readonly MAX_TOASTS = 5;\n private toastIdCounter = 0;\n private activeTimeouts = new Map<string, ReturnType<typeof setTimeout>>();\n /** Auto-dismiss timers only make sense in the browser (and `window` does not exist on the server). */\n private readonly isBrowser = isPlatformBrowser(inject(PLATFORM_ID));\n\n constructor() {\n this.toasts = this.toastList.asReadonly();\n }\n\n /**\n * Show a toast notification\n */\n show(config: ToastConfig): string {\n const toast: Toast = {\n id: this.generateId(),\n message: config.message,\n variant: config.variant || 'info',\n position: config.position || 'top-right',\n duration: config.duration !== undefined ? config.duration : 3000,\n showCloseButton: config.showCloseButton !== undefined ? config.showCloseButton : true,\n action: config.action,\n };\n\n // Add toast to list\n let currentToasts = this.toastList();\n\n // Remove oldest toast if at max capacity\n if (currentToasts.length >= this.MAX_TOASTS) {\n const oldestToast = currentToasts[0];\n if (oldestToast) {\n this.clearTimeout(oldestToast.id);\n currentToasts = currentToasts.slice(1);\n }\n }\n\n this.toastList.set([...currentToasts, toast]);\n\n // Auto-dismiss if duration > 0\n if (toast.duration > 0 && this.isBrowser) {\n const timeoutId = setTimeout(() => {\n this.close(toast.id);\n }, toast.duration);\n\n this.activeTimeouts.set(toast.id, timeoutId);\n }\n\n return toast.id;\n }\n\n /**\n * Close a specific toast\n */\n close(id: string): void {\n this.removeToast(id);\n }\n\n /**\n * Close all toasts\n */\n closeAll(): void {\n const currentToasts = this.toastList();\n currentToasts.forEach((toast) => {\n this.clearTimeout(toast.id);\n });\n this.toastList.set([]);\n }\n\n /**\n * Generate unique toast ID\n */\n private generateId(): string {\n return `toast-${++this.toastIdCounter}-${Date.now()}`;\n }\n\n /**\n * Remove a toast from the list\n */\n private removeToast(id: string): void {\n this.clearTimeout(id);\n const currentToasts = this.toastList();\n this.toastList.set(currentToasts.filter((t) => t.id !== id));\n }\n\n /**\n * Clear timeout for a toast\n */\n private clearTimeout(id: string): void {\n const timeoutId = this.activeTimeouts.get(id);\n if (timeoutId !== undefined) {\n clearTimeout(timeoutId);\n this.activeTimeouts.delete(id);\n }\n }\n}\n","import {\n Component,\n ChangeDetectionStrategy,\n input,\n output,\n computed,\n ViewEncapsulation,\n} from '@angular/core';\nimport { IconComponent, ɵprovideKitIcons } from '@lifecorner/angular-ui-kit/icon';\nimport {\n ICON_CHECK_CIRCLE,\n ICON_CIRCLE_X,\n ICON_EXCLAMATION_TRIANGLE,\n ICON_INFORMATION_CIRCLE,\n ICON_X_MARK,\n} from '@lifecorner/angular-ui-kit/icons';\nimport type { Toast, ToastVariant } from './toast.service';\n\n/**\n * Toast component for displaying ephemeral notification messages.\n *\n * Features:\n * - Semantic variants (success, warning, error, info)\n * - Auto-dismiss with configurable duration\n * - Manual dismiss with close button\n * - Icon display per variant\n * - Stacked toast positioning via ToastService + `<lc-toast-outlet />`\n * - Animated enter/exit transitions\n *\n * Usually rendered by `<lc-toast-outlet />`; can also be placed directly:\n *\n * @example\n * ```html\n * <lc-toast [toast]=\"toast\" (closed)=\"dismiss($event)\" />\n * ```\n */\n@Component({\n selector: 'lc-toast',\n standalone: true,\n imports: [IconComponent],\n viewProviders: [\n ɵprovideKitIcons([ICON_X_MARK, ICON_CHECK_CIRCLE, ICON_CIRCLE_X, ICON_EXCLAMATION_TRIANGLE, ICON_INFORMATION_CIRCLE]),\n ],\n templateUrl: './toast.component.html',\n styleUrls: ['./toast.component.scss'],\n encapsulation: ViewEncapsulation.None, // Required for dynamic variant class styling\n changeDetection: ChangeDetectionStrategy.OnPush,\n host: {\n '[class]': 'hostClasses()',\n '[attr.role]': 'ariaRole()',\n '[attr.aria-live]': 'ariaLive()',\n '[attr.aria-atomic]': 'announce() ? \"true\" : null',\n },\n})\nexport class ToastComponent {\n /**\n * Toast data\n */\n toast = input.required<Toast>();\n\n /**\n * Whether the toast is its own live region (`role=\"status\"`/`\"alert\"` on the\n * host). `lc-toast-outlet` sets this to false because its persistent live\n * regions already announce the toast — announcing twice would be noise.\n * @default true\n */\n announce = input<boolean>(true);\n\n /** Accessible name of the close button (`toast.showCloseButton`). */\n closeAriaLabel = input('Close notification');\n\n /**\n * Emitted when toast is closed\n */\n readonly closed = output<string>();\n\n /**\n * Computed host classes\n */\n protected hostClasses = computed(() => {\n const toast = this.toast();\n return `lc-toast lc-toast--${toast.variant}`;\n });\n\n /**\n * Computed container classes\n */\n protected containerClasses = computed(() => {\n return 'lc-toast__container';\n });\n\n /**\n * Computed ARIA role\n */\n protected ariaRole = computed(() => {\n if (!this.announce()) return null;\n const variant = this.toast().variant;\n return variant === 'error' || variant === 'warning' ? 'alert' : 'status';\n });\n\n /**\n * Computed ARIA live region priority\n */\n protected ariaLive = computed(() => {\n if (!this.announce()) return null;\n const variant = this.toast().variant;\n return variant === 'error' || variant === 'warning' ? 'assertive' : 'polite';\n });\n\n /**\n * Computed default icon based on variant\n */\n protected defaultIcon = computed(() => {\n const icons: Record<ToastVariant, string> = {\n success: 'check-circle',\n error: 'x-circle',\n warning: 'exclamation-triangle',\n info: 'information-circle',\n };\n return icons[this.toast().variant];\n });\n\n /**\n * Handle close button click\n */\n protected onCloseClick(event: Event): void {\n event.preventDefault();\n event.stopPropagation();\n this.closed.emit(this.toast().id);\n }\n\n /**\n * Handle action button click\n */\n protected onActionClick(event: Event): void {\n event.preventDefault();\n event.stopPropagation();\n const action = this.toast().action;\n if (action) {\n action.onClick();\n this.closed.emit(this.toast().id);\n }\n }\n}\n","<div [class]=\"containerClasses()\">\n <div class=\"lc-toast__icon\">\n <lc-icon [name]=\"defaultIcon()\" size=\"md\" [decorative]=\"true\" />\n </div>\n \n <div class=\"lc-toast__message\">\n {{ toast().message }}\n </div>\n \n @if (toast().action) {\n <button\n type=\"button\"\n class=\"lc-toast__action\"\n (click)=\"onActionClick($event)\"\n >\n {{ toast().action?.label }}\n </button>\n }\n \n @if (toast().showCloseButton) {\n <button\n type=\"button\"\n class=\"lc-toast__close\"\n [attr.aria-label]=\"closeAriaLabel()\"\n (click)=\"onCloseClick($event)\"\n >\n <lc-icon name=\"x-mark\" size=\"sm\" [decorative]=\"true\" />\n </button>\n }\n</div>\n","import { Component, ChangeDetectionStrategy, computed, inject, input } from '@angular/core';\nimport { OverlayInsetsService } from '@lifecorner/angular-ui-kit/internal/overlay';\nimport { ToastComponent } from './toast.component';\nimport { ToastService, type Toast, type ToastPosition } from './toast.service';\n\n/** Politeness of the live region a toast is announced from. */\ntype ToastLive = 'polite' | 'assertive';\n\nconst POSITIONS: readonly ToastPosition[] = [\n 'top-left',\n 'top-center',\n 'top-right',\n 'bottom-left',\n 'bottom-center',\n 'bottom-right',\n];\n\n/**\n * Renders the toasts of {@link ToastService}. Place it once in the application\n * shell; without it `ToastService.show()` has no visible effect.\n *\n * ```html\n * <lc-toast-outlet />\n * ```\n *\n * Toasts are stacked per `position`. Every position owns two live regions that\n * exist from the start — screen readers only announce content that is inserted\n * into an *existing* live region — one `polite` for success/info toasts and one\n * `assertive` for error/warning toasts. Dismissing a toast (close button,\n * action, auto-dismiss) removes it from the service.\n *\n * Offsets (3.28, AS-34): with `lc-app-shell` in the app (the outlet may sit\n * before or after it), the `top-*` stacks start below the shell's header and\n * the `bottom-*` stacks end above its bottom bar, each with the stack's 16 px\n * padding as the gap. The shell measures both in the browser (the header's\n * bottom edge with the top inset; the shown bar's top edge, from the\n * viewport's bottom, with the bottom inset). From 1024 px, where the bar is\n * hidden, the bottom offset is the safe-area inset. Without a shell, and in\n * prerendered HTML, both are `0px`, as before. Every stack is at most as high\n * as the space between the offsets and scrolls when its toasts need more\n * (200 % text), so each toast stays reachable. In the shell, prefer the\n * `bottom-*` positions: a top toast can still cover a page title placed\n * right under the header.\n *\n * CSS hooks (set them on `lc-toast-outlet` or any ancestor, e.g. `:root`;\n * each wins over the shell's value; write lengths, `0px` rather than `0`):\n * - `--lc-toast-outlet-offset-top` — the space kept free above the `top-*`\n * stacks. Default `0px`; with `lc-app-shell`: its header's bottom edge, or\n * `env(safe-area-inset-top, 0px)` without a header.\n * - `--lc-toast-outlet-offset-bottom` — the space kept free below the\n * `bottom-*` stacks. Default `0px`; with `lc-app-shell`: the shown bottom\n * bar's top edge, measured from the viewport's bottom, or\n * `env(safe-area-inset-bottom, 0px)` when no bar shows.\n */\n@Component({\n selector: 'lc-toast-outlet',\n standalone: true,\n imports: [ToastComponent],\n templateUrl: './toast-outlet.component.html',\n styleUrls: ['./toast-outlet.component.scss'],\n changeDetection: ChangeDetectionStrategy.OnPush,\n host: {\n // The shell's measured offsets (AS-34), private; `null` without a shell,\n // so the host gets no style attribute. The public hooks win in the CSS.\n '[style.--_lc-toast-outlet-frame-top]': 'insets.top()',\n '[style.--_lc-toast-outlet-frame-bottom]': 'insets.bottom()',\n },\n})\nexport class ToastOutletComponent {\n protected readonly positions = POSITIONS;\n protected readonly liveLevels: readonly ToastLive[] = ['assertive', 'polite'];\n\n private readonly toastService = inject(ToastService);\n protected readonly insets = inject(OverlayInsetsService);\n\n /** Accessible name of every toast's close button (forwarded to lc-toast). */\n readonly closeAriaLabel = input('Close notification');\n\n /** Toasts keyed by `${position}:${politeness}` in the order they were shown. */\n private readonly groups = computed(() => {\n const groups = new Map<string, Toast[]>();\n for (const toast of this.toastService.toasts()) {\n const key = `${toast.position}:${ToastOutletComponent.liveFor(toast)}`;\n const group = groups.get(key);\n if (group) {\n group.push(toast);\n } else {\n groups.set(key, [toast]);\n }\n }\n return groups;\n });\n\n protected toastsFor(position: ToastPosition, live: ToastLive): readonly Toast[] {\n return this.groups().get(`${position}:${live}`) ?? [];\n }\n\n protected onClosed(id: string): void {\n this.toastService.close(id);\n }\n\n /** Errors and warnings interrupt (assertive), everything else waits its turn. */\n private static liveFor(toast: Toast): ToastLive {\n return toast.variant === 'error' || toast.variant === 'warning' ? 'assertive' : 'polite';\n }\n}\n","@for (position of positions; track position) {\n <div class=\"lc-toast-outlet__stack\" [class]=\"'lc-toast-outlet__stack lc-toast-outlet__stack--' + position\">\n <!-- Both regions are always present (even empty) so that toasts inserted\n later are announced; the toasts themselves do not announce again. -->\n @for (live of liveLevels; track live) {\n <div class=\"lc-toast-outlet__region\" [attr.aria-live]=\"live\" aria-atomic=\"false\">\n @for (toast of toastsFor(position, live); track toast.id) {\n <lc-toast [toast]=\"toast\" [announce]=\"false\" [closeAriaLabel]=\"closeAriaLabel()\" (closed)=\"onClosed($event)\" />\n }\n </div>\n }\n </div>\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lifecorner/angular-ui-kit",
3
- "version": "3.27.2",
3
+ "version": "3.28.0",
4
4
  "description": "LifeCorner Design System – Angular UI component library",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -52,6 +52,12 @@ export type AppShellNavPlacement = 'below-header' | 'full-height';
52
52
  * content). The land and the footer run under the nav; the footer's content
53
53
  * starts at the nav width.
54
54
  *
55
+ * Toasts (3.28, AS-34): in the browser the shell tells `lc-toast-outlet` —
56
+ * wherever the app puts it, also after `</lc-app-shell>` — where its header
57
+ * ends and where the shown bar starts, so top toasts sit below the header and
58
+ * bottom toasts 16 px above the bar (from 1024 px: above the safe-area
59
+ * inset). `--lc-toast-outlet-offset-top` / `-bottom` override both.
60
+ *
55
61
  * CSS hooks (set them on `lc-app-shell` or any ancestor):
56
62
  * - `--lc-app-shell-nav-width` — the nav column from 1024 px. Default
57
63
  * `240px` (`248px` from 1440, `280px` from 1920).
@@ -104,6 +110,7 @@ export declare class AppShellComponent {
104
110
  readonly navPlacement: import("@angular/core").InputSignal<AppShellNavPlacement>;
105
111
  protected readonly shellClasses: import("@angular/core").Signal<string>;
106
112
  private readonly shell;
113
+ private readonly headerSlot;
107
114
  private readonly barSlot;
108
115
  constructor();
109
116
  static ɵfac: i0.ɵɵFactoryDeclaration<AppShellComponent, never>;
@@ -133,6 +140,14 @@ export type AppPagePaddingX = 'default' | 'header';
133
140
  * - `none` 0, `sm` 8 px, `md` 16 px at every width.
134
141
  */
135
142
  export type AppPagePaddingTop = 'default' | 'none' | 'sm' | 'md';
143
+ /**
144
+ * Width of `lc-app-page`'s content from 1280 px (3.28, AS-36).
145
+ * - `default`: the content column, centred, capped at `contentMaxWidth`.
146
+ * - `wide`: the standard column of a page with a right column, the right
147
+ * column left empty, and `[page-wide]` children running over it, up to
148
+ * 928 px. Below 1280 px, and next to a named right column, as `default`.
149
+ */
150
+ export type AppPageContentWidth = 'default' | 'wide';
136
151
  /**
137
152
  * A page inside `lc-app-shell`: the `<main>` landmark with the page's content
138
153
  * centred at a reading width, and an optional right column docked to the
@@ -150,6 +165,16 @@ export type AppPagePaddingTop = 'default' | 'none' | 'sm' | 'md';
150
165
  * inside main instead (e.g. a member row). It renders only with an
151
166
  * accessible name — `asideLabelledby` (the id of its heading) or
152
167
  * `asideAriaLabel`.
168
+ * - `reserveAside` keeps that column from 1280 px — width, tint and hairline —
169
+ * with no content and no landmark (`aria-hidden="true"`), so a page without
170
+ * right-column content keeps the same column as a page with one (3.28,
171
+ * AS-35). A named aside wins.
172
+ * - `contentWidth="wide"` (3.28, AS-36) lays the page out like a page with a
173
+ * right column, leaves the column empty and untinted, and lets children
174
+ * marked `[page-wide]` (e.g. a table's wrapper) run over it, up to 928 px;
175
+ * the other children (the page header, the next-step card) keep the
176
+ * standard column, so the title doesn't move between pages. Next to a named
177
+ * aside, and below 1280 px, it renders like `'default'`.
153
178
  * - `busy` marks the page as loading: `aria-busy="true"` on `<main>` while a
154
179
  * skeleton stands in for the content (AS-12). No visual change.
155
180
  * - `paddingX="header"` lines the content up with `lc-header` (3.15): its
@@ -220,8 +245,37 @@ export declare class AppPageComponent {
220
245
  * @default 'default'
221
246
  */
222
247
  readonly paddingTop: import("@angular/core").InputSignal<AppPagePaddingTop>;
248
+ /**
249
+ * Keeps the right column from 1280 px (360 · 400 · 460 px,
250
+ * `--lc-app-page-aside-width`) with its tint and hairline but no content
251
+ * and no landmark (`aria-hidden="true"`, no role), so pages without
252
+ * right-column content share one column with pages that have one. A named
253
+ * aside (`asideLabelledby` / `asideAriaLabel`) wins; `contentWidth="wide"`
254
+ * leaves the column empty and untinted. Below 1280 px nothing shows.
255
+ * @default false
256
+ */
257
+ readonly reserveAside: import("@angular/core").InputSignal<boolean>;
258
+ /**
259
+ * `'wide'`, from 1280 px: the page keeps the standard column of a page with
260
+ * a right column, the column stays empty and untinted, and `[page-wide]`
261
+ * children run over it, up to 928 px (or `contentMaxWidth` when it is not
262
+ * `880px`; `none` means the whole column). The other children keep the
263
+ * standard column. With a named aside, `'wide'` has no effect. Below
264
+ * 1280 px `'wide'` renders like `'default'`.
265
+ * @default 'default'
266
+ */
267
+ readonly contentWidth: import("@angular/core").InputSignal<AppPageContentWidth>;
223
268
  /** The right column renders only when it has an accessible name. */
224
269
  protected readonly hasAside: import("@angular/core").Signal<boolean>;
270
+ /** `contentWidth="wide"` takes effect only without a named aside, which it yields to. */
271
+ protected readonly wide: import("@angular/core").Signal<boolean>;
272
+ /** The empty, unnamed column (`reserveAside`), unless a named aside or `wide` takes the space. */
273
+ protected readonly reserved: import("@angular/core").Signal<boolean>;
274
+ /**
275
+ * The cap of `[page-wide]` children: `null` keeps the CSS's 928 px (also
276
+ * for the default `contentMaxWidth`), `none` means the whole column.
277
+ */
278
+ protected readonly wideMax: import("@angular/core").Signal<string | null>;
225
279
  protected readonly pageClasses: import("@angular/core").Signal<string>;
226
280
  static ɵfac: i0.ɵɵFactoryDeclaration<AppPageComponent, never>;
227
281
  static ɵcmp: i0.ɵɵComponentDeclaration<AppPageComponent, "lc-app-page", never, {
@@ -260,5 +314,15 @@ export declare class AppPageComponent {
260
314
  "required": false;
261
315
  "isSignal": true;
262
316
  };
317
+ "reserveAside": {
318
+ "alias": "reserveAside";
319
+ "required": false;
320
+ "isSignal": true;
321
+ };
322
+ "contentWidth": {
323
+ "alias": "contentWidth";
324
+ "required": false;
325
+ "isSignal": true;
326
+ };
263
327
  }, {}, never, ["*", "[page-aside]"], true, never>;
264
328
  }
@@ -28,4 +28,25 @@ export declare class OverlayStackService {
28
28
  claim(id: string, event: Event): boolean;
29
29
  static ɵfac: i0.ɵɵFactoryDeclaration<OverlayStackService, never>;
30
30
  static ɵprov: i0.ɵɵInjectableDeclaration<any>;
31
+ }
32
+ /**
33
+ * The space a fixed overlay keeps free at the top and the bottom of the
34
+ * viewport, as CSS lengths (3.28, AS-34). `lc-app-shell` writes them in the
35
+ * browser: `top` is its header's bottom edge, `bottom` the shown bottom bar's
36
+ * top edge, measured from the viewport's bottom (or the safe-area insets when
37
+ * there is no header or no bar). `lc-toast-outlet` reads them, so its stacks
38
+ * clear the header and the bar even when the app renders the outlet outside
39
+ * the shell (no custom property can inherit there).
40
+ *
41
+ * `null` means "no shell": on the server, before the shell has measured,
42
+ * without ResizeObserver and after the shell is destroyed. Internal to the
43
+ * library; not part of the public API and without a semver guarantee.
44
+ */
45
+ export declare class OverlayInsetsService {
46
+ /** CSS length kept free above top-edge overlays, e.g. `'56px'`. */
47
+ readonly top: import("@angular/core").WritableSignal<string | null>;
48
+ /** CSS length kept free below bottom-edge overlays, e.g. `'102px'`. */
49
+ readonly bottom: import("@angular/core").WritableSignal<string | null>;
50
+ static ɵfac: i0.ɵɵFactoryDeclaration<OverlayInsetsService, never>;
51
+ static ɵprov: i0.ɵɵInjectableDeclaration<any>;
31
52
  }
@@ -1,5 +1,6 @@
1
1
  import * as i0 from "@angular/core";
2
2
  import { Signal } from "@angular/core";
3
+ import { OverlayInsetsService } from "@lifecorner/angular-ui-kit/internal/overlay";
3
4
  export type ToastVariant = 'success' | 'error' | 'warning' | 'info';
4
5
  export type ToastPosition = 'top-left' | 'top-center' | 'top-right' | 'bottom-left' | 'bottom-center' | 'bottom-right';
5
6
  export interface ToastAction {
@@ -181,11 +182,35 @@ type ToastLive = 'polite' | 'assertive';
181
182
  * into an *existing* live region — one `polite` for success/info toasts and one
182
183
  * `assertive` for error/warning toasts. Dismissing a toast (close button,
183
184
  * action, auto-dismiss) removes it from the service.
185
+ *
186
+ * Offsets (3.28, AS-34): with `lc-app-shell` in the app (the outlet may sit
187
+ * before or after it), the `top-*` stacks start below the shell's header and
188
+ * the `bottom-*` stacks end above its bottom bar, each with the stack's 16 px
189
+ * padding as the gap. The shell measures both in the browser (the header's
190
+ * bottom edge with the top inset; the shown bar's top edge, from the
191
+ * viewport's bottom, with the bottom inset). From 1024 px, where the bar is
192
+ * hidden, the bottom offset is the safe-area inset. Without a shell, and in
193
+ * prerendered HTML, both are `0px`, as before. Every stack is at most as high
194
+ * as the space between the offsets and scrolls when its toasts need more
195
+ * (200 % text), so each toast stays reachable. In the shell, prefer the
196
+ * `bottom-*` positions: a top toast can still cover a page title placed
197
+ * right under the header.
198
+ *
199
+ * CSS hooks (set them on `lc-toast-outlet` or any ancestor, e.g. `:root`;
200
+ * each wins over the shell's value; write lengths, `0px` rather than `0`):
201
+ * - `--lc-toast-outlet-offset-top` — the space kept free above the `top-*`
202
+ * stacks. Default `0px`; with `lc-app-shell`: its header's bottom edge, or
203
+ * `env(safe-area-inset-top, 0px)` without a header.
204
+ * - `--lc-toast-outlet-offset-bottom` — the space kept free below the
205
+ * `bottom-*` stacks. Default `0px`; with `lc-app-shell`: the shown bottom
206
+ * bar's top edge, measured from the viewport's bottom, or
207
+ * `env(safe-area-inset-bottom, 0px)` when no bar shows.
184
208
  */
185
209
  export declare class ToastOutletComponent {
186
210
  protected readonly positions: readonly ToastPosition[];
187
211
  protected readonly liveLevels: readonly ToastLive[];
188
212
  private readonly toastService;
213
+ protected readonly insets: OverlayInsetsService;
189
214
  /** Accessible name of every toast's close button (forwarded to lc-toast). */
190
215
  readonly closeAriaLabel: import("@angular/core").InputSignal<string>;
191
216
  /** Toasts keyed by `${position}:${politeness}` in the order they were shown. */