@foliokit/cms-ui 0.0.0 → 1.0.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.
package/esm2022/index.js CHANGED
@@ -1,2 +1,7 @@
1
- export * from './lib/cms-ui/cms-ui';
1
+ export * from './lib/app-shell/app-shell.component';
2
+ export * from './lib/theme.service';
3
+ export * from './lib/shell-config.token';
4
+ export * from './lib/about-page/about-page.component';
5
+ export * from './lib/links-page/links-page.component';
6
+ export * from './lib/route-data';
2
7
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../libs/cms-ui/src/index.ts"],"names":[],"mappings":"AAAA,cAAc,qBAAqB,CAAC","sourcesContent":["export * from './lib/cms-ui/cms-ui';\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../libs/cms-ui/src/index.ts"],"names":[],"mappings":"AAAA,cAAc,qCAAqC,CAAC;AACpD,cAAc,qBAAqB,CAAC;AACpC,cAAc,0BAA0B,CAAC;AACzC,cAAc,uCAAuC,CAAC;AACtD,cAAc,uCAAuC,CAAC;AACtD,cAAc,kBAAkB,CAAC","sourcesContent":["export * from './lib/app-shell/app-shell.component';\nexport * from './lib/theme.service';\nexport * from './lib/shell-config.token';\nexport * from './lib/about-page/about-page.component';\nexport * from './lib/links-page/links-page.component';\nexport * from './lib/route-data';\n"]}
@@ -0,0 +1,81 @@
1
+ import { ChangeDetectionStrategy, Component, effect, inject, PLATFORM_ID, } from '@angular/core';
2
+ import { isPlatformBrowser } from '@angular/common';
3
+ import { ActivatedRoute } from '@angular/router';
4
+ import { toSignal } from '@angular/core/rxjs-interop';
5
+ import { map } from 'rxjs';
6
+ import { Meta, Title } from '@angular/platform-browser';
7
+ import { MarkdownModule } from 'ngx-markdown';
8
+ import * as i0 from "@angular/core";
9
+ import * as i1 from "ngx-markdown";
10
+ export class AboutPageComponent {
11
+ route = inject(ActivatedRoute);
12
+ meta = inject(Meta);
13
+ title = inject(Title);
14
+ platformId = inject(PLATFORM_ID);
15
+ page = toSignal(this.route.data.pipe(map((data) => data['page'] ?? null)), { initialValue: this.route.snapshot.data['page'] ?? null });
16
+ constructor() {
17
+ effect(() => {
18
+ const p = this.page();
19
+ if (!p)
20
+ return;
21
+ if (!isPlatformBrowser(this.platformId))
22
+ return;
23
+ this.title.setTitle(p.seo?.title ?? p.title);
24
+ if (p.seo?.description) {
25
+ this.meta.updateTag({ name: 'description', content: p.seo.description });
26
+ }
27
+ if (p.seo?.ogImage) {
28
+ this.meta.updateTag({ property: 'og:image', content: p.seo.ogImage });
29
+ }
30
+ if (p.seo?.noIndex) {
31
+ this.meta.updateTag({ name: 'robots', content: 'noindex' });
32
+ }
33
+ else {
34
+ this.meta.removeTag('name="robots"');
35
+ }
36
+ });
37
+ }
38
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: AboutPageComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
39
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.4", type: AboutPageComponent, isStandalone: true, selector: "cms-about-page", ngImport: i0, template: `
40
+ @if (page()) {
41
+ <article class="max-w-3xl mx-auto px-4 py-10">
42
+ @if (page()!.heroImageUrl) {
43
+ <img
44
+ class="w-full rounded-xl object-cover mb-8 max-h-80"
45
+ [src]="page()!.heroImageUrl"
46
+ [alt]="page()!.heroImageAlt || page()!.title"
47
+ />
48
+ }
49
+ <markdown [data]="page()!.body" class="folio-prose" />
50
+ </article>
51
+ } @else {
52
+ <p class="p-10 text-center opacity-50">No content available.</p>
53
+ }
54
+ `, isInline: true, dependencies: [{ kind: "ngmodule", type: MarkdownModule }, { kind: "component", type: i1.MarkdownComponent, selector: "markdown, [markdown]", inputs: ["data", "src", "disableSanitizer", "inline", "clipboard", "clipboardButtonComponent", "clipboardButtonTemplate", "emoji", "katex", "katexOptions", "mermaid", "mermaidOptions", "lineHighlight", "line", "lineOffset", "lineNumbers", "start", "commandLine", "filterOutput", "host", "prompt", "output", "user"], outputs: ["error", "load", "ready"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
55
+ }
56
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: AboutPageComponent, decorators: [{
57
+ type: Component,
58
+ args: [{
59
+ selector: 'cms-about-page',
60
+ standalone: true,
61
+ changeDetection: ChangeDetectionStrategy.OnPush,
62
+ imports: [MarkdownModule],
63
+ template: `
64
+ @if (page()) {
65
+ <article class="max-w-3xl mx-auto px-4 py-10">
66
+ @if (page()!.heroImageUrl) {
67
+ <img
68
+ class="w-full rounded-xl object-cover mb-8 max-h-80"
69
+ [src]="page()!.heroImageUrl"
70
+ [alt]="page()!.heroImageAlt || page()!.title"
71
+ />
72
+ }
73
+ <markdown [data]="page()!.body" class="folio-prose" />
74
+ </article>
75
+ } @else {
76
+ <p class="p-10 text-center opacity-50">No content available.</p>
77
+ }
78
+ `,
79
+ }]
80
+ }], ctorParameters: () => [] });
81
+ //# sourceMappingURL=about-page.component.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"about-page.component.js","sourceRoot":"","sources":["../../../../../../libs/cms-ui/src/lib/about-page/about-page.component.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,uBAAuB,EACvB,SAAS,EAET,MAAM,EACN,MAAM,EACN,WAAW,GACZ,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AACpD,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AACjD,OAAO,EAAE,QAAQ,EAAE,MAAM,4BAA4B,CAAC;AACtD,OAAO,EAAE,GAAG,EAAE,MAAM,MAAM,CAAC;AAC3B,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,2BAA2B,CAAC;AACxD,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;;;AAyB9C,MAAM,OAAO,kBAAkB;IACZ,KAAK,GAAG,MAAM,CAAC,cAAc,CAAC,CAAC;IAC/B,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC;IACpB,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC;IACtB,UAAU,GAAG,MAAM,CAAC,WAAW,CAAC,CAAC;IAEzC,IAAI,GAAG,QAAQ,CACtB,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAE,IAAI,CAAC,MAAM,CAAe,IAAI,IAAI,CAAC,CAAC,EACxE,EAAE,YAAY,EAAG,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAe,IAAI,IAAI,EAAE,CAC1E,CAAC;IAEF;QACE,MAAM,CAAC,GAAG,EAAE;YACV,MAAM,CAAC,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC;YACtB,IAAI,CAAC,CAAC;gBAAE,OAAO;YACf,IAAI,CAAC,iBAAiB,CAAC,IAAI,CAAC,UAAU,CAAC;gBAAE,OAAO;YAChD,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC,GAAG,EAAE,KAAK,IAAI,CAAC,CAAC,KAAK,CAAC,CAAC;YAC7C,IAAI,CAAC,CAAC,GAAG,EAAE,WAAW,EAAE,CAAC;gBACvB,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,IAAI,EAAE,aAAa,EAAE,OAAO,EAAE,CAAC,CAAC,GAAG,CAAC,WAAW,EAAE,CAAC,CAAC;YAC3E,CAAC;YACD,IAAI,CAAC,CAAC,GAAG,EAAE,OAAO,EAAE,CAAC;gBACnB,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,QAAQ,EAAE,UAAU,EAAE,OAAO,EAAE,CAAC,CAAC,GAAG,CAAC,OAAO,EAAE,CAAC,CAAC;YACxE,CAAC;YACD,IAAI,CAAC,CAAC,GAAG,EAAE,OAAO,EAAE,CAAC;gBACnB,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,SAAS,EAAE,CAAC,CAAC;YAC9D,CAAC;iBAAM,CAAC;gBACN,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,eAAe,CAAC,CAAC;YACvC,CAAC;QACH,CAAC,CAAC,CAAC;IACL,CAAC;uGA7BU,kBAAkB;2FAAlB,kBAAkB,0EAjBnB;;;;;;;;;;;;;;;GAeT,2DAhBS,cAAc;;2FAkBb,kBAAkB;kBAtB9B,SAAS;mBAAC;oBACT,QAAQ,EAAE,gBAAgB;oBAC1B,UAAU,EAAE,IAAI;oBAChB,eAAe,EAAE,uBAAuB,CAAC,MAAM;oBAC/C,OAAO,EAAE,CAAC,cAAc,CAAC;oBACzB,QAAQ,EAAE;;;;;;;;;;;;;;;GAeT;iBACF","sourcesContent":["import {\n ChangeDetectionStrategy,\n Component,\n computed,\n effect,\n inject,\n PLATFORM_ID,\n} from '@angular/core';\nimport { isPlatformBrowser } from '@angular/common';\nimport { ActivatedRoute } from '@angular/router';\nimport { toSignal } from '@angular/core/rxjs-interop';\nimport { map } from 'rxjs';\nimport { Meta, Title } from '@angular/platform-browser';\nimport { MarkdownModule } from 'ngx-markdown';\nimport type { AboutPage } from '@foliokit/cms-core';\n\n@Component({\n selector: 'cms-about-page',\n standalone: true,\n changeDetection: ChangeDetectionStrategy.OnPush,\n imports: [MarkdownModule],\n template: `\n @if (page()) {\n <article class=\"max-w-3xl mx-auto px-4 py-10\">\n @if (page()!.heroImageUrl) {\n <img\n class=\"w-full rounded-xl object-cover mb-8 max-h-80\"\n [src]=\"page()!.heroImageUrl\"\n [alt]=\"page()!.heroImageAlt || page()!.title\"\n />\n }\n <markdown [data]=\"page()!.body\" class=\"folio-prose\" />\n </article>\n } @else {\n <p class=\"p-10 text-center opacity-50\">No content available.</p>\n }\n `,\n})\nexport class AboutPageComponent {\n private readonly route = inject(ActivatedRoute);\n private readonly meta = inject(Meta);\n private readonly title = inject(Title);\n private readonly platformId = inject(PLATFORM_ID);\n\n readonly page = toSignal(\n this.route.data.pipe(map((data) => (data['page'] as AboutPage) ?? null)),\n { initialValue: (this.route.snapshot.data['page'] as AboutPage) ?? null },\n );\n\n constructor() {\n effect(() => {\n const p = this.page();\n if (!p) return;\n if (!isPlatformBrowser(this.platformId)) return;\n this.title.setTitle(p.seo?.title ?? p.title);\n if (p.seo?.description) {\n this.meta.updateTag({ name: 'description', content: p.seo.description });\n }\n if (p.seo?.ogImage) {\n this.meta.updateTag({ property: 'og:image', content: p.seo.ogImage });\n }\n if (p.seo?.noIndex) {\n this.meta.updateTag({ name: 'robots', content: 'noindex' });\n } else {\n this.meta.removeTag('name=\"robots\"');\n }\n });\n }\n}\n"]}
@@ -0,0 +1,52 @@
1
+ import { ChangeDetectionStrategy, Component, inject, signal, } from '@angular/core';
2
+ import { BreakpointObserver, Breakpoints } from '@angular/cdk/layout';
3
+ import { MatSidenavModule } from '@angular/material/sidenav';
4
+ import { MatToolbarModule } from '@angular/material/toolbar';
5
+ import { MatIconModule } from '@angular/material/icon';
6
+ import { MatButtonModule } from '@angular/material/button';
7
+ import { SHELL_CONFIG } from '../shell-config.token';
8
+ import { ThemeService } from '../theme.service';
9
+ import * as i0 from "@angular/core";
10
+ import * as i1 from "@angular/material/sidenav";
11
+ import * as i2 from "@angular/material/toolbar";
12
+ import * as i3 from "@angular/material/icon";
13
+ import * as i4 from "@angular/material/button";
14
+ export class AppShellComponent {
15
+ config = inject(SHELL_CONFIG);
16
+ theme = inject(ThemeService);
17
+ isMobile = signal(false, ...(ngDevMode ? [{ debugName: "isMobile" }] : /* istanbul ignore next */ []));
18
+ sidenavOpen = signal(false, ...(ngDevMode ? [{ debugName: "sidenavOpen" }] : /* istanbul ignore next */ []));
19
+ breakpointObserver = inject(BreakpointObserver);
20
+ bpSub;
21
+ ngOnInit() {
22
+ this.bpSub = this.breakpointObserver
23
+ .observe([Breakpoints.XSmall, Breakpoints.Small])
24
+ .subscribe((state) => {
25
+ const mobile = state.matches;
26
+ this.isMobile.set(mobile);
27
+ this.sidenavOpen.set(!mobile);
28
+ });
29
+ this.theme.apply();
30
+ }
31
+ ngOnDestroy() {
32
+ this.bpSub?.unsubscribe();
33
+ }
34
+ toggleSidenav() {
35
+ this.sidenavOpen.update((open) => !open);
36
+ }
37
+ toggleTheme() {
38
+ this.theme.toggle();
39
+ }
40
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: AppShellComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
41
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.4", type: AppShellComponent, isStandalone: true, selector: "folio-app-shell", ngImport: i0, template: "<mat-sidenav-container class=\"shell-container\">\n <mat-sidenav\n class=\"shell-sidenav\"\n [mode]=\"isMobile() ? 'over' : 'side'\"\n [opened]=\"sidenavOpen()\"\n (openedChange)=\"sidenavOpen.set($event)\"\n >\n <ng-content select=\"[shellNav]\" />\n </mat-sidenav>\n\n <mat-sidenav-content class=\"shell-content\">\n <mat-toolbar class=\"shell-toolbar\">\n @if (isMobile()) {\n <button mat-icon-button aria-label=\"Toggle navigation\" (click)=\"toggleSidenav()\">\n <mat-icon>menu</mat-icon>\n </button>\n }\n\n @if (config.logoUrl) {\n <img [src]=\"config.logoUrl\" [alt]=\"config.appName\" class=\"shell-logo\" />\n }\n <span class=\"shell-app-name\">{{ config.appName }}</span>\n\n <span class=\"flex-1\"></span>\n\n <button\n mat-icon-button\n [attr.aria-label]=\"theme.scheme() === 'dark' ? 'Switch to light mode' : 'Switch to dark mode'\"\n (click)=\"toggleTheme()\"\n >\n <mat-icon>{{ theme.scheme() === 'dark' ? 'light_mode' : 'dark_mode' }}</mat-icon>\n </button>\n\n <ng-content select=\"[shellHeaderActions]\" />\n\n @if (config.showAuth) {\n <ng-content select=\"[shellAuthSlot]\" />\n }\n </mat-toolbar>\n\n <main class=\"shell-main\">\n <ng-content />\n </main>\n </mat-sidenav-content>\n</mat-sidenav-container>\n", styles: [":host{display:block;height:100%}.shell-container{height:100%;background-color:var(--mat-sys-surface)}.shell-sidenav{width:260px;border-right:1px solid var(--mat-sys-outline-variant);background-color:var(--mat-sys-surface-container-low)}.shell-toolbar{position:sticky;top:0;z-index:100;background-color:var(--mat-sys-surface-container);border-bottom:1px solid var(--mat-sys-outline-variant);gap:4px}.shell-logo{height:32px;width:auto;margin-right:8px}.shell-app-name{font-size:1.125rem;font-weight:600;color:var(--mat-sys-on-surface);white-space:nowrap}.shell-content{display:flex;flex-direction:column;background-color:var(--mat-sys-surface)}.shell-main{flex:1;overflow-y:auto}\n"], dependencies: [{ kind: "ngmodule", type: MatSidenavModule }, { kind: "component", type: i1.MatSidenav, selector: "mat-sidenav", inputs: ["fixedInViewport", "fixedTopGap", "fixedBottomGap"], exportAs: ["matSidenav"] }, { kind: "component", type: i1.MatSidenavContainer, selector: "mat-sidenav-container", exportAs: ["matSidenavContainer"] }, { kind: "component", type: i1.MatSidenavContent, selector: "mat-sidenav-content" }, { kind: "ngmodule", type: MatToolbarModule }, { kind: "component", type: i2.MatToolbar, selector: "mat-toolbar", inputs: ["color"], exportAs: ["matToolbar"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i3.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i4.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
42
+ }
43
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: AppShellComponent, decorators: [{
44
+ type: Component,
45
+ args: [{ selector: 'folio-app-shell', changeDetection: ChangeDetectionStrategy.OnPush, standalone: true, imports: [
46
+ MatSidenavModule,
47
+ MatToolbarModule,
48
+ MatIconModule,
49
+ MatButtonModule,
50
+ ], template: "<mat-sidenav-container class=\"shell-container\">\n <mat-sidenav\n class=\"shell-sidenav\"\n [mode]=\"isMobile() ? 'over' : 'side'\"\n [opened]=\"sidenavOpen()\"\n (openedChange)=\"sidenavOpen.set($event)\"\n >\n <ng-content select=\"[shellNav]\" />\n </mat-sidenav>\n\n <mat-sidenav-content class=\"shell-content\">\n <mat-toolbar class=\"shell-toolbar\">\n @if (isMobile()) {\n <button mat-icon-button aria-label=\"Toggle navigation\" (click)=\"toggleSidenav()\">\n <mat-icon>menu</mat-icon>\n </button>\n }\n\n @if (config.logoUrl) {\n <img [src]=\"config.logoUrl\" [alt]=\"config.appName\" class=\"shell-logo\" />\n }\n <span class=\"shell-app-name\">{{ config.appName }}</span>\n\n <span class=\"flex-1\"></span>\n\n <button\n mat-icon-button\n [attr.aria-label]=\"theme.scheme() === 'dark' ? 'Switch to light mode' : 'Switch to dark mode'\"\n (click)=\"toggleTheme()\"\n >\n <mat-icon>{{ theme.scheme() === 'dark' ? 'light_mode' : 'dark_mode' }}</mat-icon>\n </button>\n\n <ng-content select=\"[shellHeaderActions]\" />\n\n @if (config.showAuth) {\n <ng-content select=\"[shellAuthSlot]\" />\n }\n </mat-toolbar>\n\n <main class=\"shell-main\">\n <ng-content />\n </main>\n </mat-sidenav-content>\n</mat-sidenav-container>\n", styles: [":host{display:block;height:100%}.shell-container{height:100%;background-color:var(--mat-sys-surface)}.shell-sidenav{width:260px;border-right:1px solid var(--mat-sys-outline-variant);background-color:var(--mat-sys-surface-container-low)}.shell-toolbar{position:sticky;top:0;z-index:100;background-color:var(--mat-sys-surface-container);border-bottom:1px solid var(--mat-sys-outline-variant);gap:4px}.shell-logo{height:32px;width:auto;margin-right:8px}.shell-app-name{font-size:1.125rem;font-weight:600;color:var(--mat-sys-on-surface);white-space:nowrap}.shell-content{display:flex;flex-direction:column;background-color:var(--mat-sys-surface)}.shell-main{flex:1;overflow-y:auto}\n"] }]
51
+ }] });
52
+ //# sourceMappingURL=app-shell.component.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"app-shell.component.js","sourceRoot":"","sources":["../../../../../../libs/cms-ui/src/lib/app-shell/app-shell.component.ts","../../../../../../libs/cms-ui/src/lib/app-shell/app-shell.component.html"],"names":[],"mappings":"AAAA,OAAO,EACL,uBAAuB,EACvB,SAAS,EACT,MAAM,EAGN,MAAM,GACP,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,kBAAkB,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAEtE,OAAO,EAAE,gBAAgB,EAAE,MAAM,2BAA2B,CAAC;AAC7D,OAAO,EAAE,gBAAgB,EAAE,MAAM,2BAA2B,CAAC;AAC7D,OAAO,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;AACvD,OAAO,EAAE,eAAe,EAAE,MAAM,0BAA0B,CAAC;AAC3D,OAAO,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AACrD,OAAO,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;;;;;;AAehD,MAAM,OAAO,iBAAiB;IACT,MAAM,GAAG,MAAM,CAAC,YAAY,CAAC,CAAC;IAC9B,KAAK,GAAG,MAAM,CAAC,YAAY,CAAC,CAAC;IAE7B,QAAQ,GAAG,MAAM,CAAC,KAAK,+EAAC,CAAC;IACzB,WAAW,GAAG,MAAM,CAAC,KAAK,kFAAC,CAAC;IAE9B,kBAAkB,GAAG,MAAM,CAAC,kBAAkB,CAAC,CAAC;IACzD,KAAK,CAAgB;IAE7B,QAAQ;QACN,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,kBAAkB;aACjC,OAAO,CAAC,CAAC,WAAW,CAAC,MAAM,EAAE,WAAW,CAAC,KAAK,CAAC,CAAC;aAChD,SAAS,CAAC,CAAC,KAAK,EAAE,EAAE;YACnB,MAAM,MAAM,GAAG,KAAK,CAAC,OAAO,CAAC;YAC7B,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;YAC1B,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,CAAC;QAChC,CAAC,CAAC,CAAC;QACL,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC;IACrB,CAAC;IAED,WAAW;QACT,IAAI,CAAC,KAAK,EAAE,WAAW,EAAE,CAAC;IAC5B,CAAC;IAES,aAAa;QACrB,IAAI,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC;IAC3C,CAAC;IAES,WAAW;QACnB,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC;IACtB,CAAC;uGA/BU,iBAAiB;2FAAjB,iBAAiB,2EC9B9B,k3CA6CA,guBDrBI,gBAAgB,0YAChB,gBAAgB,gJAChB,aAAa,mLACb,eAAe;;2FAGN,iBAAiB;kBAb7B,SAAS;+BACE,iBAAiB,mBAGV,uBAAuB,CAAC,MAAM,cACnC,IAAI,WACP;wBACP,gBAAgB;wBAChB,gBAAgB;wBAChB,aAAa;wBACb,eAAe;qBAChB","sourcesContent":["import {\n ChangeDetectionStrategy,\n Component,\n inject,\n OnDestroy,\n OnInit,\n signal,\n} from '@angular/core';\nimport { BreakpointObserver, Breakpoints } from '@angular/cdk/layout';\nimport { Subscription } from 'rxjs';\nimport { MatSidenavModule } from '@angular/material/sidenav';\nimport { MatToolbarModule } from '@angular/material/toolbar';\nimport { MatIconModule } from '@angular/material/icon';\nimport { MatButtonModule } from '@angular/material/button';\nimport { SHELL_CONFIG } from '../shell-config.token';\nimport { ThemeService } from '../theme.service';\n\n@Component({\n selector: 'folio-app-shell',\n templateUrl: './app-shell.component.html',\n styleUrl: './app-shell.component.scss',\n changeDetection: ChangeDetectionStrategy.OnPush,\n standalone: true,\n imports: [\n MatSidenavModule,\n MatToolbarModule,\n MatIconModule,\n MatButtonModule,\n ],\n})\nexport class AppShellComponent implements OnInit, OnDestroy {\n protected readonly config = inject(SHELL_CONFIG);\n protected readonly theme = inject(ThemeService);\n\n protected readonly isMobile = signal(false);\n protected readonly sidenavOpen = signal(false);\n\n private readonly breakpointObserver = inject(BreakpointObserver);\n private bpSub?: Subscription;\n\n ngOnInit(): void {\n this.bpSub = this.breakpointObserver\n .observe([Breakpoints.XSmall, Breakpoints.Small])\n .subscribe((state) => {\n const mobile = state.matches;\n this.isMobile.set(mobile);\n this.sidenavOpen.set(!mobile);\n });\n this.theme.apply();\n }\n\n ngOnDestroy(): void {\n this.bpSub?.unsubscribe();\n }\n\n protected toggleSidenav(): void {\n this.sidenavOpen.update((open) => !open);\n }\n\n protected toggleTheme(): void {\n this.theme.toggle();\n }\n}\n","<mat-sidenav-container class=\"shell-container\">\n <mat-sidenav\n class=\"shell-sidenav\"\n [mode]=\"isMobile() ? 'over' : 'side'\"\n [opened]=\"sidenavOpen()\"\n (openedChange)=\"sidenavOpen.set($event)\"\n >\n <ng-content select=\"[shellNav]\" />\n </mat-sidenav>\n\n <mat-sidenav-content class=\"shell-content\">\n <mat-toolbar class=\"shell-toolbar\">\n @if (isMobile()) {\n <button mat-icon-button aria-label=\"Toggle navigation\" (click)=\"toggleSidenav()\">\n <mat-icon>menu</mat-icon>\n </button>\n }\n\n @if (config.logoUrl) {\n <img [src]=\"config.logoUrl\" [alt]=\"config.appName\" class=\"shell-logo\" />\n }\n <span class=\"shell-app-name\">{{ config.appName }}</span>\n\n <span class=\"flex-1\"></span>\n\n <button\n mat-icon-button\n [attr.aria-label]=\"theme.scheme() === 'dark' ? 'Switch to light mode' : 'Switch to dark mode'\"\n (click)=\"toggleTheme()\"\n >\n <mat-icon>{{ theme.scheme() === 'dark' ? 'light_mode' : 'dark_mode' }}</mat-icon>\n </button>\n\n <ng-content select=\"[shellHeaderActions]\" />\n\n @if (config.showAuth) {\n <ng-content select=\"[shellAuthSlot]\" />\n }\n </mat-toolbar>\n\n <main class=\"shell-main\">\n <ng-content />\n </main>\n </mat-sidenav-content>\n</mat-sidenav-container>\n"]}
@@ -0,0 +1,143 @@
1
+ import { ChangeDetectionStrategy, Component, computed, effect, inject, PLATFORM_ID, } from '@angular/core';
2
+ import { isPlatformBrowser } from '@angular/common';
3
+ import { ActivatedRoute } from '@angular/router';
4
+ import { toSignal } from '@angular/core/rxjs-interop';
5
+ import { map } from 'rxjs';
6
+ import { Meta, Title } from '@angular/platform-browser';
7
+ import { MatButtonModule } from '@angular/material/button';
8
+ import * as i0 from "@angular/core";
9
+ const PLATFORM_ICONS = {
10
+ youtube: 'fa-brands fa-youtube',
11
+ twitch: 'fa-brands fa-twitch',
12
+ twitter: 'fa-brands fa-x-twitter',
13
+ bluesky: 'fa-brands fa-bluesky',
14
+ github: 'fa-brands fa-github',
15
+ linkedin: 'fa-brands fa-linkedin-in',
16
+ instagram: 'fa-brands fa-instagram',
17
+ tiktok: 'fa-brands fa-tiktok',
18
+ facebook: 'fa-brands fa-facebook',
19
+ email: 'fa-solid fa-envelope',
20
+ website: 'fa-solid fa-globe',
21
+ };
22
+ export class LinksPageComponent {
23
+ route = inject(ActivatedRoute);
24
+ meta = inject(Meta);
25
+ title = inject(Title);
26
+ platformId = inject(PLATFORM_ID);
27
+ page = toSignal(this.route.data.pipe(map((data) => data['page'] ?? null)), { initialValue: this.route.snapshot.data['page'] ?? null });
28
+ sortedLinks = computed(() => [...(this.page()?.links ?? [])].sort((a, b) => a.order - b.order), ...(ngDevMode ? [{ debugName: "sortedLinks" }] : /* istanbul ignore next */ []));
29
+ getIcon(link) {
30
+ if (link.icon)
31
+ return link.icon;
32
+ if (link.platform)
33
+ return PLATFORM_ICONS[link.platform] ?? '';
34
+ return '';
35
+ }
36
+ constructor() {
37
+ effect(() => {
38
+ const p = this.page();
39
+ if (!p)
40
+ return;
41
+ if (!isPlatformBrowser(this.platformId))
42
+ return;
43
+ this.title.setTitle(p.seo?.title ?? p.title);
44
+ if (p.seo?.description) {
45
+ this.meta.updateTag({ name: 'description', content: p.seo.description });
46
+ }
47
+ if (p.seo?.ogImage) {
48
+ this.meta.updateTag({ property: 'og:image', content: p.seo.ogImage });
49
+ }
50
+ if (p.seo?.noIndex) {
51
+ this.meta.updateTag({ name: 'robots', content: 'noindex' });
52
+ }
53
+ else {
54
+ this.meta.removeTag('name="robots"');
55
+ }
56
+ });
57
+ }
58
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: LinksPageComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
59
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.4", type: LinksPageComponent, isStandalone: true, selector: "cms-links-page", ngImport: i0, template: `
60
+ @if (page()) {
61
+ <div class="flex flex-col items-center max-w-md mx-auto px-4 py-12 gap-6">
62
+ @if (page()!.avatarUrl) {
63
+ <img
64
+ class="w-24 h-24 rounded-full object-cover shadow-md"
65
+ [src]="page()!.avatarUrl"
66
+ [alt]="page()!.avatarAlt || page()!.title"
67
+ />
68
+ }
69
+ @if (page()!.headline) {
70
+ <h1 class="text-2xl font-bold text-center">{{ page()!.headline }}</h1>
71
+ }
72
+ @if (page()!.bio) {
73
+ <p class="text-center opacity-70">{{ page()!.bio }}</p>
74
+ }
75
+ <nav class="flex flex-col w-full gap-3">
76
+ @for (link of sortedLinks(); track link.id) {
77
+ <a
78
+ [href]="link.url"
79
+ target="_blank"
80
+ rel="noopener noreferrer"
81
+ [class]="link.highlighted ? 'link-btn-featured' : 'link-btn'"
82
+ class="w-full flex items-center rounded-full px-5 !py-3 text-base font-medium transition-opacity no-underline"
83
+ >
84
+ <span class="w-6 flex-shrink-0 text-lg leading-none">
85
+ @if (getIcon(link)) {
86
+ <i [class]="getIcon(link)"></i>
87
+ }
88
+ </span>
89
+ <span class="flex-1 text-center">{{ link.label }}</span>
90
+ <span class="w-6 flex-shrink-0"></span>
91
+ </a>
92
+ }
93
+ </nav>
94
+ </div>
95
+ } @else {
96
+ <p class="p-10 text-center opacity-50">No content available.</p>
97
+ }
98
+ `, isInline: true, styles: [".link-btn{background-color:var(--mat-sys-primary);color:var(--mat-sys-on-primary)}.link-btn-featured{background-color:var(--mat-sys-tertiary-container);color:var(--mat-sys-on-tertiary-container)}.link-btn:hover,.link-btn-featured:hover{opacity:.92}\n"], dependencies: [{ kind: "ngmodule", type: MatButtonModule }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
99
+ }
100
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: LinksPageComponent, decorators: [{
101
+ type: Component,
102
+ args: [{ selector: 'cms-links-page', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, imports: [MatButtonModule], template: `
103
+ @if (page()) {
104
+ <div class="flex flex-col items-center max-w-md mx-auto px-4 py-12 gap-6">
105
+ @if (page()!.avatarUrl) {
106
+ <img
107
+ class="w-24 h-24 rounded-full object-cover shadow-md"
108
+ [src]="page()!.avatarUrl"
109
+ [alt]="page()!.avatarAlt || page()!.title"
110
+ />
111
+ }
112
+ @if (page()!.headline) {
113
+ <h1 class="text-2xl font-bold text-center">{{ page()!.headline }}</h1>
114
+ }
115
+ @if (page()!.bio) {
116
+ <p class="text-center opacity-70">{{ page()!.bio }}</p>
117
+ }
118
+ <nav class="flex flex-col w-full gap-3">
119
+ @for (link of sortedLinks(); track link.id) {
120
+ <a
121
+ [href]="link.url"
122
+ target="_blank"
123
+ rel="noopener noreferrer"
124
+ [class]="link.highlighted ? 'link-btn-featured' : 'link-btn'"
125
+ class="w-full flex items-center rounded-full px-5 !py-3 text-base font-medium transition-opacity no-underline"
126
+ >
127
+ <span class="w-6 flex-shrink-0 text-lg leading-none">
128
+ @if (getIcon(link)) {
129
+ <i [class]="getIcon(link)"></i>
130
+ }
131
+ </span>
132
+ <span class="flex-1 text-center">{{ link.label }}</span>
133
+ <span class="w-6 flex-shrink-0"></span>
134
+ </a>
135
+ }
136
+ </nav>
137
+ </div>
138
+ } @else {
139
+ <p class="p-10 text-center opacity-50">No content available.</p>
140
+ }
141
+ `, styles: [".link-btn{background-color:var(--mat-sys-primary);color:var(--mat-sys-on-primary)}.link-btn-featured{background-color:var(--mat-sys-tertiary-container);color:var(--mat-sys-on-tertiary-container)}.link-btn:hover,.link-btn-featured:hover{opacity:.92}\n"] }]
142
+ }], ctorParameters: () => [] });
143
+ //# sourceMappingURL=links-page.component.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"links-page.component.js","sourceRoot":"","sources":["../../../../../../libs/cms-ui/src/lib/links-page/links-page.component.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n ChangeDetectionStrategy,\n Component,\n computed,\n effect,\n inject,\n PLATFORM_ID,\n} from '@angular/core';\nimport { isPlatformBrowser } from '@angular/common';\nimport { ActivatedRoute } from '@angular/router';\nimport { toSignal } from '@angular/core/rxjs-interop';\nimport { map } from 'rxjs';\nimport { Meta, Title } from '@angular/platform-browser';\nimport { MatButtonModule } from '@angular/material/button';\nimport type { LinksPage, LinksLink } from '@foliokit/cms-core';\nimport type { SocialPlatform } from '@foliokit/cms-core';\n\nconst PLATFORM_ICONS: Record<SocialPlatform, string> = {\n youtube: 'fa-brands fa-youtube',\n twitch: 'fa-brands fa-twitch',\n twitter: 'fa-brands fa-x-twitter',\n bluesky: 'fa-brands fa-bluesky',\n github: 'fa-brands fa-github',\n linkedin: 'fa-brands fa-linkedin-in',\n instagram: 'fa-brands fa-instagram',\n tiktok: 'fa-brands fa-tiktok',\n facebook: 'fa-brands fa-facebook',\n email: 'fa-solid fa-envelope',\n website: 'fa-solid fa-globe',\n};\n\n@Component({\n selector: 'cms-links-page',\n standalone: true,\n changeDetection: ChangeDetectionStrategy.OnPush,\n imports: [MatButtonModule],\n styles: [`\n .link-btn {\n background-color: var(--mat-sys-primary);\n color: var(--mat-sys-on-primary);\n }\n .link-btn-featured {\n background-color: var(--mat-sys-tertiary-container);\n color: var(--mat-sys-on-tertiary-container);\n }\n .link-btn:hover, .link-btn-featured:hover {\n opacity: 0.92;\n }\n `],\n template: `\n @if (page()) {\n <div class=\"flex flex-col items-center max-w-md mx-auto px-4 py-12 gap-6\">\n @if (page()!.avatarUrl) {\n <img\n class=\"w-24 h-24 rounded-full object-cover shadow-md\"\n [src]=\"page()!.avatarUrl\"\n [alt]=\"page()!.avatarAlt || page()!.title\"\n />\n }\n @if (page()!.headline) {\n <h1 class=\"text-2xl font-bold text-center\">{{ page()!.headline }}</h1>\n }\n @if (page()!.bio) {\n <p class=\"text-center opacity-70\">{{ page()!.bio }}</p>\n }\n <nav class=\"flex flex-col w-full gap-3\">\n @for (link of sortedLinks(); track link.id) {\n <a\n [href]=\"link.url\"\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n [class]=\"link.highlighted ? 'link-btn-featured' : 'link-btn'\"\n class=\"w-full flex items-center rounded-full px-5 !py-3 text-base font-medium transition-opacity no-underline\"\n >\n <span class=\"w-6 flex-shrink-0 text-lg leading-none\">\n @if (getIcon(link)) {\n <i [class]=\"getIcon(link)\"></i>\n }\n </span>\n <span class=\"flex-1 text-center\">{{ link.label }}</span>\n <span class=\"w-6 flex-shrink-0\"></span>\n </a>\n }\n </nav>\n </div>\n } @else {\n <p class=\"p-10 text-center opacity-50\">No content available.</p>\n }\n `,\n})\nexport class LinksPageComponent {\n private readonly route = inject(ActivatedRoute);\n private readonly meta = inject(Meta);\n private readonly title = inject(Title);\n private readonly platformId = inject(PLATFORM_ID);\n\n readonly page = toSignal(\n this.route.data.pipe(map((data) => (data['page'] as LinksPage) ?? null)),\n { initialValue: (this.route.snapshot.data['page'] as LinksPage) ?? null },\n );\n\n readonly sortedLinks = computed<LinksLink[]>(() =>\n [...(this.page()?.links ?? [])].sort((a, b) => a.order - b.order),\n );\n\n getIcon(link: LinksLink): string {\n if (link.icon) return link.icon;\n if (link.platform) return PLATFORM_ICONS[link.platform] ?? '';\n return '';\n }\n\n constructor() {\n effect(() => {\n const p = this.page();\n if (!p) return;\n if (!isPlatformBrowser(this.platformId)) return;\n this.title.setTitle(p.seo?.title ?? p.title);\n if (p.seo?.description) {\n this.meta.updateTag({ name: 'description', content: p.seo.description });\n }\n if (p.seo?.ogImage) {\n this.meta.updateTag({ property: 'og:image', content: p.seo.ogImage });\n }\n if (p.seo?.noIndex) {\n this.meta.updateTag({ name: 'robots', content: 'noindex' });\n } else {\n this.meta.removeTag('name=\"robots\"');\n }\n });\n }\n}\n"]}
@@ -0,0 +1,17 @@
1
+ /**
2
+ * The route data key used by AboutPageComponent and LinksPageComponent
3
+ * to read their page from Angular Router resolved data.
4
+ *
5
+ * Use this constant in your route definition's `resolve` map so the key
6
+ * stays in sync with what the components expect:
7
+ *
8
+ * ```ts
9
+ * {
10
+ * path: 'about',
11
+ * component: AboutPageComponent,
12
+ * resolve: { [CMS_ROUTE_DATA_KEY]: aboutPageResolver }
13
+ * }
14
+ * ```
15
+ */
16
+ export const CMS_ROUTE_DATA_KEY = 'page';
17
+ //# sourceMappingURL=route-data.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"route-data.js","sourceRoot":"","sources":["../../../../../libs/cms-ui/src/lib/route-data.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;GAcG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG,MAAe,CAAC","sourcesContent":["import type { AboutPage, LinksPage } from '@foliokit/cms-core';\n\n/**\n * The route data key used by AboutPageComponent and LinksPageComponent\n * to read their page from Angular Router resolved data.\n *\n * Use this constant in your route definition's `resolve` map so the key\n * stays in sync with what the components expect:\n *\n * ```ts\n * {\n * path: 'about',\n * component: AboutPageComponent,\n * resolve: { [CMS_ROUTE_DATA_KEY]: aboutPageResolver }\n * }\n * ```\n */\nexport const CMS_ROUTE_DATA_KEY = 'page' as const;\n\n/**\n * Shape of the resolved route data expected by AboutPageComponent.\n * Use as the return type annotation on your resolver:\n *\n * ```ts\n * export const aboutPageResolver: ResolveFn<AboutPageRouteData[typeof CMS_ROUTE_DATA_KEY]> = ...\n * ```\n */\nexport interface AboutPageRouteData {\n [CMS_ROUTE_DATA_KEY]: AboutPage | null;\n}\n\n/**\n * Shape of the resolved route data expected by LinksPageComponent.\n */\nexport interface LinksPageRouteData {\n [CMS_ROUTE_DATA_KEY]: LinksPage | null;\n}\n"]}
@@ -0,0 +1,3 @@
1
+ import { InjectionToken } from '@angular/core';
2
+ export const SHELL_CONFIG = new InjectionToken('SHELL_CONFIG');
3
+ //# sourceMappingURL=shell-config.token.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"shell-config.token.js","sourceRoot":"","sources":["../../../../../libs/cms-ui/src/lib/shell-config.token.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAU/C,MAAM,CAAC,MAAM,YAAY,GAAG,IAAI,cAAc,CAAc,cAAc,CAAC,CAAC","sourcesContent":["import { InjectionToken } from '@angular/core';\nimport type { NavItem } from '@foliokit/cms-core';\n\nexport interface ShellConfig {\n appName: string;\n logoUrl?: string;\n showAuth?: boolean;\n nav?: NavItem[];\n}\n\nexport const SHELL_CONFIG = new InjectionToken<ShellConfig>('SHELL_CONFIG');\n"]}
@@ -0,0 +1,45 @@
1
+ import { inject, Injectable, PLATFORM_ID, signal } from '@angular/core';
2
+ import { isPlatformBrowser } from '@angular/common';
3
+ import * as i0 from "@angular/core";
4
+ const STORAGE_KEY = 'folio-theme';
5
+ export class ThemeService {
6
+ platformId = inject(PLATFORM_ID);
7
+ scheme = signal(this.resolveInitialScheme(), ...(ngDevMode ? [{ debugName: "scheme" }] : /* istanbul ignore next */ []));
8
+ toggle() {
9
+ this.scheme.update((s) => (s === 'light' ? 'dark' : 'light'));
10
+ this.apply();
11
+ }
12
+ apply() {
13
+ if (!isPlatformBrowser(this.platformId))
14
+ return;
15
+ const current = this.scheme();
16
+ document.documentElement.setAttribute('data-theme', current);
17
+ try {
18
+ localStorage.setItem(STORAGE_KEY, current);
19
+ }
20
+ catch {
21
+ // localStorage may be unavailable (private browsing, etc.)
22
+ }
23
+ }
24
+ resolveInitialScheme() {
25
+ if (!isPlatformBrowser(this.platformId))
26
+ return 'light';
27
+ try {
28
+ const stored = localStorage.getItem(STORAGE_KEY);
29
+ if (stored === 'light' || stored === 'dark')
30
+ return stored;
31
+ }
32
+ catch {
33
+ // ignore
34
+ }
35
+ const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
36
+ return prefersDark ? 'dark' : 'light';
37
+ }
38
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: ThemeService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
39
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: ThemeService, providedIn: 'root' });
40
+ }
41
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: ThemeService, decorators: [{
42
+ type: Injectable,
43
+ args: [{ providedIn: 'root' }]
44
+ }] });
45
+ //# sourceMappingURL=theme.service.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"theme.service.js","sourceRoot":"","sources":["../../../../../libs/cms-ui/src/lib/theme.service.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AACxE,OAAO,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;;AAIpD,MAAM,WAAW,GAAG,aAAa,CAAC;AAGlC,MAAM,OAAO,YAAY;IACN,UAAU,GAAG,MAAM,CAAC,WAAW,CAAC,CAAC;IAEzC,MAAM,GAAG,MAAM,CAAc,IAAI,CAAC,oBAAoB,EAAE,6EAAC,CAAC;IAEnE,MAAM;QACJ,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC;QAC9D,IAAI,CAAC,KAAK,EAAE,CAAC;IACf,CAAC;IAED,KAAK;QACH,IAAI,CAAC,iBAAiB,CAAC,IAAI,CAAC,UAAU,CAAC;YAAE,OAAO;QAChD,MAAM,OAAO,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC;QAC9B,QAAQ,CAAC,eAAe,CAAC,YAAY,CAAC,YAAY,EAAE,OAAO,CAAC,CAAC;QAC7D,IAAI,CAAC;YACH,YAAY,CAAC,OAAO,CAAC,WAAW,EAAE,OAAO,CAAC,CAAC;QAC7C,CAAC;QAAC,MAAM,CAAC;YACP,2DAA2D;QAC7D,CAAC;IACH,CAAC;IAEO,oBAAoB;QAC1B,IAAI,CAAC,iBAAiB,CAAC,IAAI,CAAC,UAAU,CAAC;YAAE,OAAO,OAAO,CAAC;QAExD,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,YAAY,CAAC,OAAO,CAAC,WAAW,CAAuB,CAAC;YACvE,IAAI,MAAM,KAAK,OAAO,IAAI,MAAM,KAAK,MAAM;gBAAE,OAAO,MAAM,CAAC;QAC7D,CAAC;QAAC,MAAM,CAAC;YACP,SAAS;QACX,CAAC;QAED,MAAM,WAAW,GAAG,MAAM,CAAC,UAAU,CAAC,8BAA8B,CAAC,CAAC,OAAO,CAAC;QAC9E,OAAO,WAAW,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC;IACxC,CAAC;uGAjCU,YAAY;2GAAZ,YAAY,cADC,MAAM;;2FACnB,YAAY;kBADxB,UAAU;mBAAC,EAAE,UAAU,EAAE,MAAM,EAAE","sourcesContent":["import { inject, Injectable, PLATFORM_ID, signal } from '@angular/core';\nimport { isPlatformBrowser } from '@angular/common';\n\nexport type ColorScheme = 'light' | 'dark';\n\nconst STORAGE_KEY = 'folio-theme';\n\n@Injectable({ providedIn: 'root' })\nexport class ThemeService {\n private readonly platformId = inject(PLATFORM_ID);\n\n readonly scheme = signal<ColorScheme>(this.resolveInitialScheme());\n\n toggle(): void {\n this.scheme.update((s) => (s === 'light' ? 'dark' : 'light'));\n this.apply();\n }\n\n apply(): void {\n if (!isPlatformBrowser(this.platformId)) return;\n const current = this.scheme();\n document.documentElement.setAttribute('data-theme', current);\n try {\n localStorage.setItem(STORAGE_KEY, current);\n } catch {\n // localStorage may be unavailable (private browsing, etc.)\n }\n }\n\n private resolveInitialScheme(): ColorScheme {\n if (!isPlatformBrowser(this.platformId)) return 'light';\n\n try {\n const stored = localStorage.getItem(STORAGE_KEY) as ColorScheme | null;\n if (stored === 'light' || stored === 'dark') return stored;\n } catch {\n // ignore\n }\n\n const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;\n return prefersDark ? 'dark' : 'light';\n }\n}\n"]}
package/index.d.ts CHANGED
@@ -1 +1,6 @@
1
- export * from './lib/cms-ui/cms-ui';
1
+ export * from './lib/app-shell/app-shell.component';
2
+ export * from './lib/theme.service';
3
+ export * from './lib/shell-config.token';
4
+ export * from './lib/about-page/about-page.component';
5
+ export * from './lib/links-page/links-page.component';
6
+ export * from './lib/route-data';
@@ -0,0 +1,12 @@
1
+ import type { AboutPage } from '@foliokit/cms-core';
2
+ import * as i0 from "@angular/core";
3
+ export declare class AboutPageComponent {
4
+ private readonly route;
5
+ private readonly meta;
6
+ private readonly title;
7
+ private readonly platformId;
8
+ readonly page: import("@angular/core").Signal<AboutPage>;
9
+ constructor();
10
+ static ɵfac: i0.ɵɵFactoryDeclaration<AboutPageComponent, never>;
11
+ static ɵcmp: i0.ɵɵComponentDeclaration<AboutPageComponent, "cms-about-page", never, {}, {}, never, never, true, never>;
12
+ }
@@ -0,0 +1,17 @@
1
+ import { OnDestroy, OnInit } from '@angular/core';
2
+ import { ThemeService } from '../theme.service';
3
+ import * as i0 from "@angular/core";
4
+ export declare class AppShellComponent implements OnInit, OnDestroy {
5
+ protected readonly config: import("@foliokit/cms-ui").ShellConfig;
6
+ protected readonly theme: ThemeService;
7
+ protected readonly isMobile: import("@angular/core").WritableSignal<boolean>;
8
+ protected readonly sidenavOpen: import("@angular/core").WritableSignal<boolean>;
9
+ private readonly breakpointObserver;
10
+ private bpSub?;
11
+ ngOnInit(): void;
12
+ ngOnDestroy(): void;
13
+ protected toggleSidenav(): void;
14
+ protected toggleTheme(): void;
15
+ static ɵfac: i0.ɵɵFactoryDeclaration<AppShellComponent, never>;
16
+ static ɵcmp: i0.ɵɵComponentDeclaration<AppShellComponent, "folio-app-shell", never, {}, {}, never, ["[shellNav]", "[shellHeaderActions]", "[shellAuthSlot]", "*"], true, never>;
17
+ }
@@ -0,0 +1,14 @@
1
+ import type { LinksPage, LinksLink } from '@foliokit/cms-core';
2
+ import * as i0 from "@angular/core";
3
+ export declare class LinksPageComponent {
4
+ private readonly route;
5
+ private readonly meta;
6
+ private readonly title;
7
+ private readonly platformId;
8
+ readonly page: import("@angular/core").Signal<LinksPage>;
9
+ readonly sortedLinks: import("@angular/core").Signal<LinksLink[]>;
10
+ getIcon(link: LinksLink): string;
11
+ constructor();
12
+ static ɵfac: i0.ɵɵFactoryDeclaration<LinksPageComponent, never>;
13
+ static ɵcmp: i0.ɵɵComponentDeclaration<LinksPageComponent, "cms-links-page", never, {}, {}, never, never, true, never>;
14
+ }
@@ -0,0 +1,34 @@
1
+ import type { AboutPage, LinksPage } from '@foliokit/cms-core';
2
+ /**
3
+ * The route data key used by AboutPageComponent and LinksPageComponent
4
+ * to read their page from Angular Router resolved data.
5
+ *
6
+ * Use this constant in your route definition's `resolve` map so the key
7
+ * stays in sync with what the components expect:
8
+ *
9
+ * ```ts
10
+ * {
11
+ * path: 'about',
12
+ * component: AboutPageComponent,
13
+ * resolve: { [CMS_ROUTE_DATA_KEY]: aboutPageResolver }
14
+ * }
15
+ * ```
16
+ */
17
+ export declare const CMS_ROUTE_DATA_KEY: "page";
18
+ /**
19
+ * Shape of the resolved route data expected by AboutPageComponent.
20
+ * Use as the return type annotation on your resolver:
21
+ *
22
+ * ```ts
23
+ * export const aboutPageResolver: ResolveFn<AboutPageRouteData[typeof CMS_ROUTE_DATA_KEY]> = ...
24
+ * ```
25
+ */
26
+ export interface AboutPageRouteData {
27
+ [CMS_ROUTE_DATA_KEY]: AboutPage | null;
28
+ }
29
+ /**
30
+ * Shape of the resolved route data expected by LinksPageComponent.
31
+ */
32
+ export interface LinksPageRouteData {
33
+ [CMS_ROUTE_DATA_KEY]: LinksPage | null;
34
+ }
@@ -0,0 +1,9 @@
1
+ import { InjectionToken } from '@angular/core';
2
+ import type { NavItem } from '@foliokit/cms-core';
3
+ export interface ShellConfig {
4
+ appName: string;
5
+ logoUrl?: string;
6
+ showAuth?: boolean;
7
+ nav?: NavItem[];
8
+ }
9
+ export declare const SHELL_CONFIG: InjectionToken<ShellConfig>;
@@ -0,0 +1,11 @@
1
+ import * as i0 from "@angular/core";
2
+ export type ColorScheme = 'light' | 'dark';
3
+ export declare class ThemeService {
4
+ private readonly platformId;
5
+ readonly scheme: import("@angular/core").WritableSignal<ColorScheme>;
6
+ toggle(): void;
7
+ apply(): void;
8
+ private resolveInitialScheme;
9
+ static ɵfac: i0.ɵɵFactoryDeclaration<ThemeService, never>;
10
+ static ɵprov: i0.ɵɵInjectableDeclaration<ThemeService>;
11
+ }
package/package.json CHANGED
@@ -1,12 +1,32 @@
1
1
  {
2
2
  "name": "@foliokit/cms-ui",
3
- "version": "0.0.0",
3
+ "version": "1.0.0",
4
+ "description": "Angular shell layout components and theme service for FolioKit CMS",
5
+ "keywords": [
6
+ "angular",
7
+ "cms",
8
+ "foliokit",
9
+ "app-shell",
10
+ "material"
11
+ ],
12
+ "license": "MIT",
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "https://github.com/dougwilliamson/foliokit"
16
+ },
4
17
  "publishConfig": {
5
18
  "access": "public"
6
19
  },
7
20
  "peerDependencies": {
8
- "@angular/common": "^21.1.0",
9
- "@angular/core": "^21.1.0"
21
+ "@angular/cdk": "^21.2.2",
22
+ "@angular/common": "^21.2.4",
23
+ "@angular/core": "^21.2.4",
24
+ "@angular/material": "^21.2.2",
25
+ "@angular/platform-browser": "^21.2.4",
26
+ "@angular/router": "^21.2.4",
27
+ "@foliokit/cms-core": "^1.0.0",
28
+ "ngx-markdown": "^21.1.0",
29
+ "rxjs": "~7.8.0"
10
30
  },
11
31
  "sideEffects": false,
12
32
  "module": "esm2022/foliokit-cms-ui.js",
@@ -1,11 +0,0 @@
1
- import { Component } from '@angular/core';
2
- import * as i0 from "@angular/core";
3
- export class CmsUi {
4
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.6", ngImport: i0, type: CmsUi, deps: [], target: i0.ɵɵFactoryTarget.Component });
5
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.1.6", type: CmsUi, isStandalone: true, selector: "lib-cms-ui", ngImport: i0, template: "<p>CmsUi works!</p>\n", styles: [""] });
6
- }
7
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.6", ngImport: i0, type: CmsUi, decorators: [{
8
- type: Component,
9
- args: [{ selector: 'lib-cms-ui', imports: [], template: "<p>CmsUi works!</p>\n" }]
10
- }] });
11
- //# sourceMappingURL=cms-ui.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"cms-ui.js","sourceRoot":"","sources":["../../../../../../libs/cms-ui/src/lib/cms-ui/cms-ui.ts","../../../../../../libs/cms-ui/src/lib/cms-ui/cms-ui.html"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;;AAQ1C,MAAM,OAAO,KAAK;uGAAL,KAAK;2FAAL,KAAK,sECRlB,uBACA;;2FDOa,KAAK;kBANjB,SAAS;+BACE,YAAY,WACb,EAAE","sourcesContent":["import { Component } from '@angular/core';\n\n@Component({\n selector: 'lib-cms-ui',\n imports: [],\n templateUrl: './cms-ui.html',\n styleUrl: './cms-ui.scss',\n})\nexport class CmsUi {}\n","<p>CmsUi works!</p>\n"]}
@@ -1,5 +0,0 @@
1
- import * as i0 from "@angular/core";
2
- export declare class CmsUi {
3
- static ɵfac: i0.ɵɵFactoryDeclaration<CmsUi, never>;
4
- static ɵcmp: i0.ɵɵComponentDeclaration<CmsUi, "lib-cms-ui", never, {}, {}, never, never, true, never>;
5
- }