@lukfel/ng-scaffold 22.1.2 → 22.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -482,7 +482,7 @@ public listItems = signal<ListItem[]>([
482
482
  { id: 2, matIcon: 'person', title: 'Item 3', subtitle: 'I have no edit buton', hiddenButtonIds: ['edit'] },
483
483
  ]);
484
484
 
485
- public buttons = signal<Button[]>([ // (Optional) list buttons
485
+ public listButtons = signal<Button[]>([ // (Optional) list buttons
486
486
  { id: 'edit', matIcon: 'edit' },
487
487
  { id: 'delete', matIcon: 'delete', cssClass: 'warn' }
488
488
  ]);
@@ -490,7 +490,7 @@ public buttons = signal<Button[]>([ // (Optional) list buttons
490
490
  // (Optional) Handle sort events
491
491
  public onListSortChange(event: { sortToken: string, sortAsc: boolean }): void {
492
492
  if (event?.sortToken === 'title') {
493
- this.listItems.sort((a, b) => {
493
+ this.listItems().sort((a, b) => {
494
494
  if (!a.title || !b.title) return 0;
495
495
  if (event.sortAsc) return a.title.localeCompare(b.title);
496
496
  return b.title.localeCompare(a.title);
@@ -1,6 +1,6 @@
1
+ import { MediaMatcher, BreakpointObserver, Breakpoints } from '@angular/cdk/layout';
1
2
  import * as i0 from '@angular/core';
2
- import { InjectionToken, makeEnvironmentProviders, inject, Injectable, PLATFORM_ID, DOCUMENT, signal, input, output, ChangeDetectionStrategy, Component, effect, DestroyRef, viewChild, computed, model, afterNextRender, linkedSignal, TemplateRef, Directive, contentChild } from '@angular/core';
3
- import { BreakpointObserver, Breakpoints } from '@angular/cdk/layout';
3
+ import { inject, PLATFORM_ID, REQUEST, Injectable, InjectionToken, makeEnvironmentProviders, DOCUMENT, signal, input, output, ChangeDetectionStrategy, Component, effect, DestroyRef, viewChild, computed, model, afterNextRender, linkedSignal, TemplateRef, Directive, contentChild } from '@angular/core';
4
4
  import { isPlatformBrowser, NgClass, NgTemplateOutlet, KeyValuePipe } from '@angular/common';
5
5
  import { toObservable, toSignal, takeUntilDestroyed } from '@angular/core/rxjs-interop';
6
6
  import * as i1$3 from '@angular/router';
@@ -45,9 +45,89 @@ import { MatRippleModule } from '@angular/material/core';
45
45
  import * as i8 from '@angular/material/divider';
46
46
  import { MatDividerModule } from '@angular/material/divider';
47
47
 
48
+ /**
49
+ * Viewports assumed during SSR. A request reveals a device class, never an actual viewport, and
50
+ * CDK exposes only query strings, so these are plausible device sizes rather than derived values.
51
+ * They have to keep landing in `Breakpoints.XSmall` and `Breakpoints.Large`, which the spec asserts.
52
+ */
53
+ const SSR_MOBILE_VIEWPORT = { width: 390, height: 844 };
54
+ const SSR_DESKTOP_VIEWPORT = { width: 1280, height: 800 };
55
+ const MOBILE_USER_AGENT_REGEX = /Android|iPhone|iPod|Windows Phone|IEMobile|BlackBerry|Opera Mini/i;
56
+ /**
57
+ * Answers media queries during SSR from the incoming request rather than matching nothing, so
58
+ * breakpoint driven markup is server rendered for the requesting device class instead of always
59
+ * falling back to desktop. On the browser it defers to the platform implementation.
60
+ *
61
+ * Opt in with `provideScaffold({ ssrBreakpoints: true })`. Doing so makes the SSR response depend
62
+ * on the request, so whatever serves it must `Vary` on `Sec-CH-UA-Mobile` and `User-Agent` or a
63
+ * cached mobile render will be handed to a desktop client.
64
+ */
65
+ class ScaffoldMediaMatcher extends MediaMatcher {
66
+ constructor() {
67
+ super(...arguments);
68
+ this.platformId = inject(PLATFORM_ID);
69
+ this.request = inject(REQUEST, { optional: true });
70
+ }
71
+ matchMedia(query) {
72
+ if (isPlatformBrowser(this.platformId))
73
+ return super.matchMedia(query);
74
+ const { width, height } = this.isMobileRequest() ? SSR_MOBILE_VIEWPORT : SSR_DESKTOP_VIEWPORT;
75
+ return {
76
+ matches: matchesMediaQuery(query, width, height),
77
+ media: query,
78
+ onchange: null,
79
+ addListener: () => { },
80
+ removeListener: () => { },
81
+ addEventListener: () => { },
82
+ removeEventListener: () => { },
83
+ dispatchEvent: () => false,
84
+ };
85
+ }
86
+ // Sec-CH-UA-Mobile needs no Accept-CH opt in on Chromium; other browsers only give a User-Agent
87
+ isMobileRequest() {
88
+ const headers = this.request?.headers;
89
+ if (!headers)
90
+ return false;
91
+ const hint = headers.get('sec-ch-ua-mobile');
92
+ if (hint)
93
+ return hint.includes('?1');
94
+ return MOBILE_USER_AGENT_REGEX.test(headers.get('user-agent') ?? '');
95
+ }
96
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: ScaffoldMediaMatcher, deps: null, target: i0.ɵɵFactoryTarget.Injectable }); }
97
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: ScaffoldMediaMatcher }); }
98
+ }
99
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: ScaffoldMediaMatcher, decorators: [{
100
+ type: Injectable
101
+ }] });
102
+ /**
103
+ * Evaluates the width and orientation parts of a media query against an assumed viewport.
104
+ * Conditions a request cannot answer, such as `prefers-color-scheme`, never match.
105
+ */
106
+ function matchesMediaQuery(query, width, height) {
107
+ // A comma separated query matches when any of its branches matches
108
+ return query.split(',').some((branch) => branch.split(' and ').every((condition) => {
109
+ const min = condition.match(/min-width:\s*([\d.]+)px/);
110
+ if (min)
111
+ return width >= parseFloat(min[1]);
112
+ const max = condition.match(/max-width:\s*([\d.]+)px/);
113
+ if (max)
114
+ return width <= parseFloat(max[1]);
115
+ if (/orientation:\s*portrait/.test(condition))
116
+ return height >= width;
117
+ if (/orientation:\s*landscape/.test(condition))
118
+ return width > height;
119
+ return false;
120
+ }));
121
+ }
122
+
48
123
  const CONFIG = new InjectionToken('config');
49
124
  function provideScaffold(config = {}) {
50
- return makeEnvironmentProviders([{ provide: CONFIG, useValue: config }]);
125
+ const providers = [{ provide: CONFIG, useValue: config }];
126
+ // Opt in, because it makes the SSR response depend on the request and therefore on Vary
127
+ if (config.ssrBreakpoints) {
128
+ providers.push({ provide: MediaMatcher, useClass: ScaffoldMediaMatcher });
129
+ }
130
+ return makeEnvironmentProviders(providers);
51
131
  }
52
132
 
53
133
  class BreakpointService {
@@ -905,11 +985,11 @@ class ContentTitleCardComponent {
905
985
  this.backButtonClickEvent.emit();
906
986
  }
907
987
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: ContentTitleCardComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
908
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.5", type: ContentTitleCardComponent, isStandalone: true, selector: "lf-content-title-card", inputs: { libraryConfig: { classPropertyName: "libraryConfig", publicName: "libraryConfig", isSignal: true, isRequired: false, transformFunction: null }, contentTitleCardConfig: { classPropertyName: "contentTitleCardConfig", publicName: "contentTitleCardConfig", isSignal: true, isRequired: false, transformFunction: null }, isMobile: { classPropertyName: "isMobile", publicName: "isMobile", isSignal: true, isRequired: false, transformFunction: null }, routeHistory: { classPropertyName: "routeHistory", publicName: "routeHistory", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { backButtonClickEvent: "backButtonClickEvent" }, ngImport: i0, template: "@if (contentTitleCardConfig(); as config) {\r\n @if (config.enable) {\r\n <mat-card\r\n class=\"lf-content-title-card mat-elevation-z2\"\r\n [class.lf-content-title-card-mobile]=\"isMobile()\"\r\n [class.px-4]=\"!isMobile()\"\r\n [class.px-2]=\"isMobile()\"\r\n [ngClass]=\"config.cssClass\">\r\n <!-- back button -->\r\n @if (config.showBackButton && routeHistory().length > 0) {\r\n <button mat-icon-button color=\"accent\" (click)=\"backButtonClicked()\">\r\n <mat-icon>arrow_back_ios</mat-icon>\r\n </button>\r\n }\r\n <!-- spacer -->\r\n <div class=\"flex-auto\"></div>\r\n <!-- label -->\r\n <span class=\"lf-content-title-card-label\">\r\n {{ config.label || '' }}\r\n </span>\r\n <!-- spacer -->\r\n <div class=\"flex-auto\"></div>\r\n <!-- empty button spacer -->\r\n <div\r\n [style.width]=\"config.showBackButton && routeHistory().length > 1 ? '48px' : '0px'\"></div>\r\n </mat-card>\r\n }\r\n}\r\n", styles: [".lf-content-title-card{z-index:var(--content-title-card-z-index);height:var(--content-title-card-height);display:flex;flex-flow:row nowrap;align-items:center;border-radius:0}.lf-content-title-card.lf-content-title-card-mobile .lf-content-title-card-label{font-size:var(--content-title-card-label-font-size-mobile)}.lf-content-title-card .lf-content-title-card-label{font-size:var(--content-title-card-label-font-size);text-align:center;overflow:hidden;text-overflow:ellipsis;white-space:normal;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical}\n"], dependencies: [{ kind: "ngmodule", type: MatCardModule }, { kind: "component", type: i1$1.MatCard, selector: "mat-card", inputs: ["appearance"], exportAs: ["matCard"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i1.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i2.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
988
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.5", type: ContentTitleCardComponent, isStandalone: true, selector: "lf-content-title-card", inputs: { libraryConfig: { classPropertyName: "libraryConfig", publicName: "libraryConfig", isSignal: true, isRequired: false, transformFunction: null }, contentTitleCardConfig: { classPropertyName: "contentTitleCardConfig", publicName: "contentTitleCardConfig", isSignal: true, isRequired: false, transformFunction: null }, isMobile: { classPropertyName: "isMobile", publicName: "isMobile", isSignal: true, isRequired: false, transformFunction: null }, routeHistory: { classPropertyName: "routeHistory", publicName: "routeHistory", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { backButtonClickEvent: "backButtonClickEvent" }, ngImport: i0, template: "@if (contentTitleCardConfig(); as config) {\r\n @if (config.enable) {\r\n <mat-card\r\n class=\"lf-content-title-card mat-elevation-z2\"\r\n [class.lf-content-title-card-mobile]=\"isMobile()\"\r\n [class.px-4]=\"!isMobile()\"\r\n [class.px-2]=\"isMobile()\"\r\n [ngClass]=\"config.cssClass\">\r\n <!-- back button -->\r\n @if (config.showBackButton && routeHistory().length > 0) {\r\n <button mat-icon-button color=\"accent\" (click)=\"backButtonClicked()\">\r\n <mat-icon>arrow_back_ios</mat-icon>\r\n </button>\r\n }\r\n <!-- spacer -->\r\n <div class=\"flex-auto\"></div>\r\n <!-- label -->\r\n <span class=\"lf-content-title-card-label\">\r\n {{ config.label || '' }}\r\n </span>\r\n <!-- spacer -->\r\n <div class=\"flex-auto\"></div>\r\n <!-- empty button spacer -->\r\n <div\r\n [style.width]=\"config.showBackButton && routeHistory().length > 0 ? '48px' : '0px'\"></div>\r\n </mat-card>\r\n }\r\n}\r\n", styles: [".lf-content-title-card{z-index:var(--content-title-card-z-index);height:var(--content-title-card-height);display:flex;flex-flow:row nowrap;align-items:center;border-radius:0}.lf-content-title-card.lf-content-title-card-mobile .lf-content-title-card-label{font-size:var(--content-title-card-label-font-size-mobile)}.lf-content-title-card .lf-content-title-card-label{font-size:var(--content-title-card-label-font-size);text-align:center;overflow:hidden;text-overflow:ellipsis;white-space:normal;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical}\n"], dependencies: [{ kind: "ngmodule", type: MatCardModule }, { kind: "component", type: i1$1.MatCard, selector: "mat-card", inputs: ["appearance"], exportAs: ["matCard"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i1.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i2.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
909
989
  }
910
990
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: ContentTitleCardComponent, decorators: [{
911
991
  type: Component,
912
- args: [{ selector: 'lf-content-title-card', changeDetection: ChangeDetectionStrategy.OnPush, standalone: true, imports: [MatCardModule, MatButtonModule, MatIconModule, NgClass], template: "@if (contentTitleCardConfig(); as config) {\r\n @if (config.enable) {\r\n <mat-card\r\n class=\"lf-content-title-card mat-elevation-z2\"\r\n [class.lf-content-title-card-mobile]=\"isMobile()\"\r\n [class.px-4]=\"!isMobile()\"\r\n [class.px-2]=\"isMobile()\"\r\n [ngClass]=\"config.cssClass\">\r\n <!-- back button -->\r\n @if (config.showBackButton && routeHistory().length > 0) {\r\n <button mat-icon-button color=\"accent\" (click)=\"backButtonClicked()\">\r\n <mat-icon>arrow_back_ios</mat-icon>\r\n </button>\r\n }\r\n <!-- spacer -->\r\n <div class=\"flex-auto\"></div>\r\n <!-- label -->\r\n <span class=\"lf-content-title-card-label\">\r\n {{ config.label || '' }}\r\n </span>\r\n <!-- spacer -->\r\n <div class=\"flex-auto\"></div>\r\n <!-- empty button spacer -->\r\n <div\r\n [style.width]=\"config.showBackButton && routeHistory().length > 1 ? '48px' : '0px'\"></div>\r\n </mat-card>\r\n }\r\n}\r\n", styles: [".lf-content-title-card{z-index:var(--content-title-card-z-index);height:var(--content-title-card-height);display:flex;flex-flow:row nowrap;align-items:center;border-radius:0}.lf-content-title-card.lf-content-title-card-mobile .lf-content-title-card-label{font-size:var(--content-title-card-label-font-size-mobile)}.lf-content-title-card .lf-content-title-card-label{font-size:var(--content-title-card-label-font-size);text-align:center;overflow:hidden;text-overflow:ellipsis;white-space:normal;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical}\n"] }]
992
+ args: [{ selector: 'lf-content-title-card', changeDetection: ChangeDetectionStrategy.OnPush, standalone: true, imports: [MatCardModule, MatButtonModule, MatIconModule, NgClass], template: "@if (contentTitleCardConfig(); as config) {\r\n @if (config.enable) {\r\n <mat-card\r\n class=\"lf-content-title-card mat-elevation-z2\"\r\n [class.lf-content-title-card-mobile]=\"isMobile()\"\r\n [class.px-4]=\"!isMobile()\"\r\n [class.px-2]=\"isMobile()\"\r\n [ngClass]=\"config.cssClass\">\r\n <!-- back button -->\r\n @if (config.showBackButton && routeHistory().length > 0) {\r\n <button mat-icon-button color=\"accent\" (click)=\"backButtonClicked()\">\r\n <mat-icon>arrow_back_ios</mat-icon>\r\n </button>\r\n }\r\n <!-- spacer -->\r\n <div class=\"flex-auto\"></div>\r\n <!-- label -->\r\n <span class=\"lf-content-title-card-label\">\r\n {{ config.label || '' }}\r\n </span>\r\n <!-- spacer -->\r\n <div class=\"flex-auto\"></div>\r\n <!-- empty button spacer -->\r\n <div\r\n [style.width]=\"config.showBackButton && routeHistory().length > 0 ? '48px' : '0px'\"></div>\r\n </mat-card>\r\n }\r\n}\r\n", styles: [".lf-content-title-card{z-index:var(--content-title-card-z-index);height:var(--content-title-card-height);display:flex;flex-flow:row nowrap;align-items:center;border-radius:0}.lf-content-title-card.lf-content-title-card-mobile .lf-content-title-card-label{font-size:var(--content-title-card-label-font-size-mobile)}.lf-content-title-card .lf-content-title-card-label{font-size:var(--content-title-card-label-font-size);text-align:center;overflow:hidden;text-overflow:ellipsis;white-space:normal;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical}\n"] }]
913
993
  }], propDecorators: { libraryConfig: [{ type: i0.Input, args: [{ isSignal: true, alias: "libraryConfig", required: false }] }], contentTitleCardConfig: [{ type: i0.Input, args: [{ isSignal: true, alias: "contentTitleCardConfig", required: false }] }], isMobile: [{ type: i0.Input, args: [{ isSignal: true, alias: "isMobile", required: false }] }], routeHistory: [{ type: i0.Input, args: [{ isSignal: true, alias: "routeHistory", required: false }] }], backButtonClickEvent: [{ type: i0.Output, args: ["backButtonClickEvent"] }] } });
914
994
 
915
995
  class DrawerComponent {
@@ -1845,5 +1925,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImpor
1845
1925
  * Generated bundle index. Do not edit.
1846
1926
  */
1847
1927
 
1848
- export { BreakpointService, CONFIG, ColorPickerComponent, DialogService, FileUploadComponent, ListComponent, ListItemAvatarDirective, ListItemButtonsDirective, ListItemSubtitleDirective, ListItemTitleDirective, LocalStorageService, Logger, NotificationComponent, OverlayService, PlaceholderComponent, RouterService, ScaffoldComponent, ScaffoldLoadingInterceptor, ScaffoldService, SeoService, SnackbarService, ThemeService, TranslationService, provideScaffold };
1928
+ export { BreakpointService, CONFIG, ColorPickerComponent, DialogService, FileUploadComponent, ListComponent, ListItemAvatarDirective, ListItemButtonsDirective, ListItemSubtitleDirective, ListItemTitleDirective, LocalStorageService, Logger, NotificationComponent, OverlayService, PlaceholderComponent, RouterService, ScaffoldComponent, ScaffoldLoadingInterceptor, ScaffoldMediaMatcher, ScaffoldService, SeoService, SnackbarService, ThemeService, TranslationService, matchesMediaQuery, provideScaffold };
1849
1929
  //# sourceMappingURL=lukfel-ng-scaffold.mjs.map