@foliokit/cms-ui 0.4.2 → 1.0.1
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 +155 -0
- package/eslint.config.mjs +48 -0
- package/ng-package.json +19 -0
- package/package.json +12 -19
- package/project.json +32 -0
- package/src/index.ts +7 -0
- package/src/lib/about-page/about-page.component.ts +219 -0
- package/src/lib/app-shell/app-shell.component.html +57 -0
- package/src/lib/app-shell/app-shell.component.scss +204 -0
- package/src/lib/app-shell/app-shell.component.ts +88 -0
- package/src/lib/app-shell/shell-nav-footer.directive.ts +4 -0
- package/src/lib/cms-ui/cms-ui.html +1 -0
- package/src/lib/cms-ui/cms-ui.scss +0 -0
- package/src/lib/cms-ui/cms-ui.spec.ts +93 -0
- package/src/lib/cms-ui/cms-ui.ts +9 -0
- package/src/lib/links-page/links-page.component.ts +256 -0
- package/src/lib/route-data.ts +27 -0
- package/src/lib/shell-config.token.ts +11 -0
- package/src/lib/theme.service.ts +44 -0
- package/src/styles/_focus.scss +17 -0
- package/src/styles/_reset.scss +65 -0
- package/src/styles/_theme.scss +76 -0
- package/src/styles/_tokens.scss +216 -0
- package/src/styles/_typography.scss +146 -0
- package/src/styles/index.scss +5 -0
- package/src/styles/tokens.css +135 -0
- package/tsconfig.json +31 -0
- package/tsconfig.lib.json +12 -0
- package/tsconfig.lib.prod.json +9 -0
- package/tsconfig.spec.json +8 -0
- package/esm2022/foliokit-cms-ui.js +0 -5
- package/esm2022/foliokit-cms-ui.js.map +0 -1
- package/esm2022/index.js +0 -4
- package/esm2022/index.js.map +0 -1
- package/esm2022/lib/app-shell/app-shell.component.js +0 -52
- package/esm2022/lib/app-shell/app-shell.component.js.map +0 -1
- package/esm2022/lib/shell-config.token.js +0 -3
- package/esm2022/lib/shell-config.token.js.map +0 -1
- package/esm2022/lib/theme.service.js +0 -45
- package/esm2022/lib/theme.service.js.map +0 -1
- package/foliokit-cms-ui.d.ts +0 -5
- package/index.d.ts +0 -3
- package/lib/app-shell/app-shell.component.d.ts +0 -17
- package/lib/shell-config.token.d.ts +0 -9
- package/lib/theme.service.d.ts +0 -11
package/README.md
CHANGED
|
@@ -1,3 +1,158 @@
|
|
|
1
1
|
# @foliokit/cms-ui
|
|
2
2
|
Part of the [Folio](https://github.com/doug-williamson/foliokit) ecosystem.
|
|
3
3
|
> This package is in early development. API is unstable.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## ⚠️ Required: Angular Material theme setup
|
|
8
|
+
|
|
9
|
+
`AppShellComponent` and all page components use Angular Material. Due to how
|
|
10
|
+
Angular Material M3 theming works, the `@include mat.theme(...)` mixin **must
|
|
11
|
+
be called in your application's global stylesheet** — it cannot be bundled
|
|
12
|
+
inside a library.
|
|
13
|
+
|
|
14
|
+
If you skip this step, Material components will render without colour, elevation,
|
|
15
|
+
or typography. This is the most common issue new consumers encounter.
|
|
16
|
+
|
|
17
|
+
Add the following to your global `styles.scss`:
|
|
18
|
+
|
|
19
|
+
```scss
|
|
20
|
+
@use '@angular/material' as mat;
|
|
21
|
+
|
|
22
|
+
// Light theme (also serves as the default)
|
|
23
|
+
html,
|
|
24
|
+
html[data-theme='light'] {
|
|
25
|
+
@include mat.theme((
|
|
26
|
+
color: (
|
|
27
|
+
theme-type: light,
|
|
28
|
+
primary: mat.$cyan-palette,
|
|
29
|
+
tertiary: mat.$cyan-palette,
|
|
30
|
+
),
|
|
31
|
+
typography: 'Plus Jakarta Sans',
|
|
32
|
+
density: 0,
|
|
33
|
+
));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Dark theme
|
|
37
|
+
html[data-theme='dark'] {
|
|
38
|
+
@include mat.theme((
|
|
39
|
+
color: (
|
|
40
|
+
theme-type: dark,
|
|
41
|
+
primary: mat.$cyan-palette,
|
|
42
|
+
tertiary: mat.$cyan-palette,
|
|
43
|
+
),
|
|
44
|
+
typography: 'Plus Jakarta Sans',
|
|
45
|
+
density: 0,
|
|
46
|
+
));
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
`ThemeService` (exported from this package) manages the `[data-theme]` attribute
|
|
51
|
+
on `<html>` and persists the user's preference to `localStorage`.
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## Getting started
|
|
56
|
+
|
|
57
|
+
### 1. Provide the shell configuration
|
|
58
|
+
|
|
59
|
+
In your `app.config.ts`, register `SHELL_CONFIG` alongside `provideFolioKit()`:
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
import { provideFolioKit } from '@foliokit/cms-core';
|
|
63
|
+
import { SHELL_CONFIG } from '@foliokit/cms-ui';
|
|
64
|
+
|
|
65
|
+
export const appConfig: ApplicationConfig = {
|
|
66
|
+
providers: [
|
|
67
|
+
provideRouter(routes),
|
|
68
|
+
provideAnimationsAsync(),
|
|
69
|
+
provideHttpClient(withFetch()),
|
|
70
|
+
provideMarkdown(),
|
|
71
|
+
provideFolioKit({ firebaseConfig: environment.firebase }),
|
|
72
|
+
{
|
|
73
|
+
provide: SHELL_CONFIG,
|
|
74
|
+
useValue: {
|
|
75
|
+
appName: 'My Site',
|
|
76
|
+
nav: [
|
|
77
|
+
{ label: 'Blog', path: '/blog' },
|
|
78
|
+
{ label: 'About', path: '/about' },
|
|
79
|
+
],
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
],
|
|
83
|
+
};
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### 2. Wrap your app in the shell
|
|
87
|
+
|
|
88
|
+
```ts
|
|
89
|
+
import { AppShellComponent } from '@foliokit/cms-ui';
|
|
90
|
+
|
|
91
|
+
@Component({
|
|
92
|
+
selector: 'app-root',
|
|
93
|
+
standalone: true,
|
|
94
|
+
imports: [AppShellComponent, RouterOutlet],
|
|
95
|
+
template: `
|
|
96
|
+
<folio-app-shell>
|
|
97
|
+
<router-outlet />
|
|
98
|
+
</folio-app-shell>
|
|
99
|
+
`,
|
|
100
|
+
})
|
|
101
|
+
export class AppComponent {}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### 3. Import design tokens
|
|
105
|
+
|
|
106
|
+
In `angular.json` (or `project.json`) styles array:
|
|
107
|
+
|
|
108
|
+
```jsonc
|
|
109
|
+
"styles": [
|
|
110
|
+
"node_modules/@foliokit/cms-ui/styles/tokens.css",
|
|
111
|
+
"src/styles.scss"
|
|
112
|
+
]
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Or via SCSS:
|
|
116
|
+
|
|
117
|
+
```scss
|
|
118
|
+
@use '@foliokit/cms-ui/styles/tokens';
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
## Design Tokens
|
|
124
|
+
|
|
125
|
+
`@foliokit/cms-ui` ships a set of CSS custom properties that define the FolioKit
|
|
126
|
+
design system: color palette, semantic theme tokens (light/dark), typography,
|
|
127
|
+
border radii, shadows, and component tokens.
|
|
128
|
+
|
|
129
|
+
Tokens resolve against the `[data-theme]` attribute on a parent element (typically
|
|
130
|
+
`<html>`). Use `ThemeService` to switch programmatically:
|
|
131
|
+
|
|
132
|
+
```ts
|
|
133
|
+
import { ThemeService } from '@foliokit/cms-ui';
|
|
134
|
+
|
|
135
|
+
// In a component or service:
|
|
136
|
+
readonly theme = inject(ThemeService);
|
|
137
|
+
|
|
138
|
+
toggleDark() {
|
|
139
|
+
this.theme.toggle();
|
|
140
|
+
}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### Token Categories
|
|
144
|
+
|
|
145
|
+
| Prefix | Purpose |
|
|
146
|
+
|--------|---------|
|
|
147
|
+
| `--slate-*`, `--cloud-*`, `--teal-*`, `--violet-*` | Base color palette |
|
|
148
|
+
| `--bg`, `--bg-subtle` | Page backgrounds |
|
|
149
|
+
| `--surface-0` .. `--surface-3` | Elevated surface layers |
|
|
150
|
+
| `--border`, `--border-strong`, `--border-accent` | Borders |
|
|
151
|
+
| `--text-primary`, `--text-secondary`, `--text-muted`, `--text-accent` | Typography colors |
|
|
152
|
+
| `--btn-primary-bg`, `--btn-primary-text`, `--btn-primary-hover` | Button tokens |
|
|
153
|
+
| `--logo-bg`, `--logo-text`, `--logo-dot` | Logo tokens |
|
|
154
|
+
| `--shadow-sm` .. `--shadow-xl` | Elevation shadows |
|
|
155
|
+
| `--focus-ring`, `--focus-border` | Focus indicators |
|
|
156
|
+
| `--nav-active-bg`, `--nav-active-color` | Navigation highlights |
|
|
157
|
+
| `--font-display`, `--font-body`, `--font-mono` | Font stacks |
|
|
158
|
+
| `--r-xs` .. `--r-2xl` | Border radii |
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import nx from '@nx/eslint-plugin';
|
|
2
|
+
import baseConfig from '../../eslint.config.mjs';
|
|
3
|
+
|
|
4
|
+
export default [
|
|
5
|
+
...baseConfig,
|
|
6
|
+
{
|
|
7
|
+
files: ['**/*.json'],
|
|
8
|
+
rules: {
|
|
9
|
+
'@nx/dependency-checks': [
|
|
10
|
+
'error',
|
|
11
|
+
{
|
|
12
|
+
ignoredFiles: ['{projectRoot}/eslint.config.{js,cjs,mjs,ts,cts,mts}'],
|
|
13
|
+
},
|
|
14
|
+
],
|
|
15
|
+
},
|
|
16
|
+
languageOptions: {
|
|
17
|
+
parser: await import('jsonc-eslint-parser'),
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
...nx.configs['flat/angular'],
|
|
21
|
+
...nx.configs['flat/angular-template'],
|
|
22
|
+
{
|
|
23
|
+
files: ['**/*.ts'],
|
|
24
|
+
rules: {
|
|
25
|
+
'@angular-eslint/directive-selector': [
|
|
26
|
+
'error',
|
|
27
|
+
{
|
|
28
|
+
type: 'attribute',
|
|
29
|
+
prefix: 'lib',
|
|
30
|
+
style: 'camelCase',
|
|
31
|
+
},
|
|
32
|
+
],
|
|
33
|
+
'@angular-eslint/component-selector': [
|
|
34
|
+
'error',
|
|
35
|
+
{
|
|
36
|
+
type: 'element',
|
|
37
|
+
prefix: 'lib',
|
|
38
|
+
style: 'kebab-case',
|
|
39
|
+
},
|
|
40
|
+
],
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
files: ['**/*.html'],
|
|
45
|
+
// Override or add rules here
|
|
46
|
+
rules: {},
|
|
47
|
+
},
|
|
48
|
+
];
|
package/ng-package.json
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "../../node_modules/ng-packagr/ng-package.schema.json",
|
|
3
|
+
"dest": "../../dist/libs/cms-ui",
|
|
4
|
+
"lib": {
|
|
5
|
+
"entryFile": "src/index.ts"
|
|
6
|
+
},
|
|
7
|
+
"assets": [
|
|
8
|
+
{
|
|
9
|
+
"input": "src/styles",
|
|
10
|
+
"glob": "**/*.scss",
|
|
11
|
+
"output": "styles"
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
"input": "src/styles",
|
|
15
|
+
"glob": "tokens.css",
|
|
16
|
+
"output": "styles"
|
|
17
|
+
}
|
|
18
|
+
]
|
|
19
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@foliokit/cms-ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "Angular shell layout components and theme service for FolioKit CMS",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"angular",
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
"license": "MIT",
|
|
13
13
|
"repository": {
|
|
14
14
|
"type": "git",
|
|
15
|
-
"url": "https://github.com/dougwilliamson/foliokit"
|
|
15
|
+
"url": "git+https://github.com/dougwilliamson/foliokit.git"
|
|
16
16
|
},
|
|
17
17
|
"publishConfig": {
|
|
18
18
|
"access": "public"
|
|
@@ -22,26 +22,19 @@
|
|
|
22
22
|
"@angular/common": "^21.2.4",
|
|
23
23
|
"@angular/core": "^21.2.4",
|
|
24
24
|
"@angular/material": "^21.2.2",
|
|
25
|
-
"@
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
"
|
|
29
|
-
|
|
30
|
-
}
|
|
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"
|
|
31
30
|
},
|
|
32
31
|
"sideEffects": false,
|
|
33
|
-
"module": "esm2022/foliokit-cms-ui.js",
|
|
34
|
-
"typings": "foliokit-cms-ui.d.ts",
|
|
35
32
|
"exports": {
|
|
36
|
-
"./
|
|
37
|
-
"
|
|
33
|
+
"./styles": {
|
|
34
|
+
"sass": "./styles/index.scss"
|
|
38
35
|
},
|
|
39
|
-
"
|
|
40
|
-
"
|
|
41
|
-
"default": "./esm2022/foliokit-cms-ui.js"
|
|
36
|
+
"./styles/*": {
|
|
37
|
+
"sass": "./styles/_*.scss"
|
|
42
38
|
}
|
|
43
|
-
},
|
|
44
|
-
"dependencies": {
|
|
45
|
-
"tslib": "^2.3.0"
|
|
46
39
|
}
|
|
47
|
-
}
|
|
40
|
+
}
|
package/project.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "cms-ui",
|
|
3
|
+
"$schema": "../../node_modules/nx/schemas/project-schema.json",
|
|
4
|
+
"sourceRoot": "libs/cms-ui/src",
|
|
5
|
+
"prefix": "lib",
|
|
6
|
+
"projectType": "library",
|
|
7
|
+
"tags": [],
|
|
8
|
+
"targets": {
|
|
9
|
+
"build": {
|
|
10
|
+
"executor": "@nx/angular:ng-packagr-lite",
|
|
11
|
+
"outputs": ["{workspaceRoot}/dist/{projectRoot}"],
|
|
12
|
+
"options": {
|
|
13
|
+
"project": "libs/cms-ui/ng-package.json",
|
|
14
|
+
"tsConfig": "libs/cms-ui/tsconfig.lib.json"
|
|
15
|
+
},
|
|
16
|
+
"configurations": {
|
|
17
|
+
"production": {
|
|
18
|
+
"tsConfig": "libs/cms-ui/tsconfig.lib.prod.json"
|
|
19
|
+
},
|
|
20
|
+
"development": {}
|
|
21
|
+
},
|
|
22
|
+
"defaultConfiguration": "production"
|
|
23
|
+
},
|
|
24
|
+
"test": {
|
|
25
|
+
"executor": "@nx/angular:unit-test",
|
|
26
|
+
"options": {}
|
|
27
|
+
},
|
|
28
|
+
"lint": {
|
|
29
|
+
"executor": "@nx/eslint:lint"
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export * from './lib/app-shell/app-shell.component';
|
|
2
|
+
export * from './lib/app-shell/shell-nav-footer.directive';
|
|
3
|
+
export * from './lib/theme.service';
|
|
4
|
+
export * from './lib/shell-config.token';
|
|
5
|
+
export * from './lib/about-page/about-page.component';
|
|
6
|
+
export * from './lib/links-page/links-page.component';
|
|
7
|
+
export * from './lib/route-data';
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ChangeDetectionStrategy,
|
|
3
|
+
Component,
|
|
4
|
+
computed,
|
|
5
|
+
effect,
|
|
6
|
+
inject,
|
|
7
|
+
PLATFORM_ID,
|
|
8
|
+
} from '@angular/core';
|
|
9
|
+
import { isPlatformBrowser } from '@angular/common';
|
|
10
|
+
import { ActivatedRoute } from '@angular/router';
|
|
11
|
+
import { toSignal } from '@angular/core/rxjs-interop';
|
|
12
|
+
import { map } from 'rxjs';
|
|
13
|
+
import { Meta, Title } from '@angular/platform-browser';
|
|
14
|
+
import { MarkdownModule } from 'ngx-markdown';
|
|
15
|
+
import { MatIconModule } from '@angular/material/icon';
|
|
16
|
+
import type { AboutPageConfig } from '@foliokit/cms-core';
|
|
17
|
+
import { ThemeService } from '../theme.service';
|
|
18
|
+
|
|
19
|
+
@Component({
|
|
20
|
+
selector: 'cms-about-page',
|
|
21
|
+
standalone: true,
|
|
22
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
23
|
+
imports: [MarkdownModule, MatIconModule],
|
|
24
|
+
styles: [`
|
|
25
|
+
:host { display: block; }
|
|
26
|
+
|
|
27
|
+
.about-container {
|
|
28
|
+
max-width: 600px;
|
|
29
|
+
margin: 48px auto;
|
|
30
|
+
padding: 0 24px;
|
|
31
|
+
display: flex;
|
|
32
|
+
flex-direction: column;
|
|
33
|
+
align-items: center;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.avatar--xl {
|
|
37
|
+
width: 96px;
|
|
38
|
+
height: 96px;
|
|
39
|
+
border-radius: 50%;
|
|
40
|
+
background: var(--logo-bg);
|
|
41
|
+
color: var(--logo-text);
|
|
42
|
+
font-family: var(--font-body);
|
|
43
|
+
font-weight: 600;
|
|
44
|
+
font-size: 32px;
|
|
45
|
+
display: flex;
|
|
46
|
+
align-items: center;
|
|
47
|
+
justify-content: center;
|
|
48
|
+
overflow: hidden;
|
|
49
|
+
flex-shrink: 0;
|
|
50
|
+
|
|
51
|
+
img {
|
|
52
|
+
width: 100%;
|
|
53
|
+
height: 100%;
|
|
54
|
+
object-fit: cover;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.about-name {
|
|
59
|
+
font-family: var(--font-display);
|
|
60
|
+
font-size: 1.5rem;
|
|
61
|
+
font-weight: 600;
|
|
62
|
+
letter-spacing: -0.015em;
|
|
63
|
+
color: var(--text-primary);
|
|
64
|
+
text-align: center;
|
|
65
|
+
margin-top: 16px;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.about-tagline {
|
|
69
|
+
font-size: 16px;
|
|
70
|
+
line-height: 1.75;
|
|
71
|
+
color: var(--text-secondary);
|
|
72
|
+
text-align: center;
|
|
73
|
+
max-width: 480px;
|
|
74
|
+
margin: 8px auto 0;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.social-links {
|
|
78
|
+
display: flex;
|
|
79
|
+
justify-content: center;
|
|
80
|
+
flex-wrap: wrap;
|
|
81
|
+
gap: 8px;
|
|
82
|
+
margin-top: 20px;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.social-link {
|
|
86
|
+
display: inline-flex;
|
|
87
|
+
align-items: center;
|
|
88
|
+
gap: 6px;
|
|
89
|
+
border: 1px solid var(--border-strong);
|
|
90
|
+
border-radius: var(--r-md);
|
|
91
|
+
padding: 6px 12px;
|
|
92
|
+
font-size: 12px;
|
|
93
|
+
font-weight: 500;
|
|
94
|
+
color: var(--text-secondary);
|
|
95
|
+
background: var(--surface-0);
|
|
96
|
+
text-decoration: none;
|
|
97
|
+
transition: background 0.12s, color 0.12s;
|
|
98
|
+
|
|
99
|
+
mat-icon {
|
|
100
|
+
font-size: 18px;
|
|
101
|
+
width: 18px;
|
|
102
|
+
height: 18px;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
&:hover {
|
|
106
|
+
background: var(--surface-2);
|
|
107
|
+
color: var(--text-primary);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
.about-prose {
|
|
112
|
+
width: 100%;
|
|
113
|
+
margin-top: 32px;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
.about-divider {
|
|
117
|
+
width: 100%;
|
|
118
|
+
border: none;
|
|
119
|
+
border-top: 1px solid var(--border);
|
|
120
|
+
margin: 24px 0 0;
|
|
121
|
+
}
|
|
122
|
+
`],
|
|
123
|
+
template: `
|
|
124
|
+
@if (about()) {
|
|
125
|
+
<div class="about-container">
|
|
126
|
+
<div class="avatar--xl">
|
|
127
|
+
@if (avatarSrc()) {
|
|
128
|
+
<img [src]="avatarSrc()" [alt]="about()!.photoAlt || about()!.headline" />
|
|
129
|
+
} @else {
|
|
130
|
+
{{ initials() }}
|
|
131
|
+
}
|
|
132
|
+
</div>
|
|
133
|
+
|
|
134
|
+
<h1 class="about-name">{{ about()!.headline }}</h1>
|
|
135
|
+
|
|
136
|
+
@if (about()!.subheadline) {
|
|
137
|
+
<p class="about-tagline">{{ about()!.subheadline }}</p>
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
@if (about()!.socialLinks?.length) {
|
|
141
|
+
<div class="social-links">
|
|
142
|
+
@for (link of about()!.socialLinks; track link.url) {
|
|
143
|
+
<a
|
|
144
|
+
class="social-link"
|
|
145
|
+
[href]="link.url"
|
|
146
|
+
target="_blank"
|
|
147
|
+
rel="noopener noreferrer"
|
|
148
|
+
>
|
|
149
|
+
<mat-icon>link</mat-icon>
|
|
150
|
+
{{ link.label || link.platform }}
|
|
151
|
+
</a>
|
|
152
|
+
}
|
|
153
|
+
</div>
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
<hr class="about-divider" />
|
|
157
|
+
|
|
158
|
+
<div class="about-prose folio-prose">
|
|
159
|
+
<markdown [data]="about()!.bio" />
|
|
160
|
+
</div>
|
|
161
|
+
</div>
|
|
162
|
+
} @else {
|
|
163
|
+
<p style="padding: 40px; text-align: center; color: var(--text-muted)">No content available.</p>
|
|
164
|
+
}
|
|
165
|
+
`,
|
|
166
|
+
})
|
|
167
|
+
export class AboutPageComponent {
|
|
168
|
+
private readonly route = inject(ActivatedRoute);
|
|
169
|
+
private readonly meta = inject(Meta);
|
|
170
|
+
private readonly title = inject(Title);
|
|
171
|
+
private readonly platformId = inject(PLATFORM_ID);
|
|
172
|
+
readonly theme = inject(ThemeService);
|
|
173
|
+
|
|
174
|
+
readonly about = toSignal(
|
|
175
|
+
this.route.data.pipe(map((data) => (data['about'] as AboutPageConfig) ?? null)),
|
|
176
|
+
{ initialValue: (this.route.snapshot.data['about'] as AboutPageConfig) ?? null },
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
protected readonly avatarSrc = computed(() =>
|
|
180
|
+
this.theme.isDark() && this.about()?.photoUrlDark
|
|
181
|
+
? this.about()!.photoUrlDark!
|
|
182
|
+
: this.about()!.photoUrl,
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
protected readonly initials = computed(() => {
|
|
186
|
+
const headline = this.about()?.headline ?? '';
|
|
187
|
+
return headline
|
|
188
|
+
.split(' ')
|
|
189
|
+
.slice(0, 2)
|
|
190
|
+
.map((w: string) => w[0])
|
|
191
|
+
.join('')
|
|
192
|
+
.toUpperCase();
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
constructor() {
|
|
196
|
+
effect(() => {
|
|
197
|
+
const a = this.about();
|
|
198
|
+
if (!a) return;
|
|
199
|
+
if (!isPlatformBrowser(this.platformId)) return;
|
|
200
|
+
|
|
201
|
+
this.title.setTitle(a.seo?.title ?? a.headline);
|
|
202
|
+
|
|
203
|
+
if (a.seo?.description) {
|
|
204
|
+
this.meta.updateTag({ name: 'description', content: a.seo.description });
|
|
205
|
+
}
|
|
206
|
+
if (a.seo?.ogImage) {
|
|
207
|
+
this.meta.updateTag({ property: 'og:image', content: a.seo.ogImage });
|
|
208
|
+
}
|
|
209
|
+
if (a.seo?.canonicalUrl) {
|
|
210
|
+
this.meta.updateTag({ rel: 'canonical', href: a.seo.canonicalUrl });
|
|
211
|
+
}
|
|
212
|
+
if (a.seo?.noIndex) {
|
|
213
|
+
this.meta.updateTag({ name: 'robots', content: 'noindex' });
|
|
214
|
+
} else {
|
|
215
|
+
this.meta.removeTag('name="robots"');
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
<div class="folio-shell-container">
|
|
2
|
+
<mat-sidenav-container>
|
|
3
|
+
<mat-sidenav
|
|
4
|
+
[mode]="isMobile() ? 'over' : 'side'"
|
|
5
|
+
[opened]="sidenavOpen()"
|
|
6
|
+
[class.icon-rail]="isIconRail()"
|
|
7
|
+
(openedChange)="sidenavOpen.set($event)"
|
|
8
|
+
>
|
|
9
|
+
<div class="folio-nav-wrapper">
|
|
10
|
+
<div class="folio-nav-body">
|
|
11
|
+
<ng-content select="[shellNav]" />
|
|
12
|
+
</div>
|
|
13
|
+
@if (navFooter) {
|
|
14
|
+
<div class="folio-nav-footer">
|
|
15
|
+
<ng-content select="[shellNavFooter]" />
|
|
16
|
+
</div>
|
|
17
|
+
}
|
|
18
|
+
</div>
|
|
19
|
+
</mat-sidenav>
|
|
20
|
+
|
|
21
|
+
<mat-sidenav-content class="folio-content">
|
|
22
|
+
<mat-toolbar class="folio-toolbar">
|
|
23
|
+
@if (isMobile()) {
|
|
24
|
+
<button mat-icon-button aria-label="Toggle navigation" (click)="toggleSidenav()">
|
|
25
|
+
<mat-icon>menu</mat-icon>
|
|
26
|
+
</button>
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
<div class="folio-logo-mark">
|
|
30
|
+
<span class="folio-logo-mark-f">F</span>
|
|
31
|
+
<div class="folio-logo-dot"></div>
|
|
32
|
+
</div>
|
|
33
|
+
<span class="folio-app-name">{{ config.appName }}</span>
|
|
34
|
+
|
|
35
|
+
<span class="flex-1"></span>
|
|
36
|
+
|
|
37
|
+
<ng-content select="[shellHeaderActions]" />
|
|
38
|
+
|
|
39
|
+
@if (config.showAuth) {
|
|
40
|
+
<ng-content select="[shellAuthSlot]" />
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
<button
|
|
44
|
+
mat-icon-button
|
|
45
|
+
[attr.aria-label]="theme.scheme() === 'dark' ? 'Switch to light mode' : 'Switch to dark mode'"
|
|
46
|
+
(click)="toggleTheme()"
|
|
47
|
+
>
|
|
48
|
+
<mat-icon>{{ theme.scheme() === 'dark' ? 'light_mode' : 'dark_mode' }}</mat-icon>
|
|
49
|
+
</button>
|
|
50
|
+
</mat-toolbar>
|
|
51
|
+
|
|
52
|
+
<main class="shell-main">
|
|
53
|
+
<ng-content />
|
|
54
|
+
</main>
|
|
55
|
+
</mat-sidenav-content>
|
|
56
|
+
</mat-sidenav-container>
|
|
57
|
+
</div>
|