weg-shared-layout 0.0.23 → 0.0.25

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.
Files changed (57) hide show
  1. package/LICENSE +21 -21
  2. package/dist/cjs/loader.cjs.js +1 -1
  3. package/dist/cjs/weg-footer_2.cjs.entry.js +307 -219
  4. package/dist/cjs/weg-shared-layout.cjs.js +1 -1
  5. package/dist/collection/collection-manifest.json +0 -1
  6. package/dist/collection/components/weg-footer/icons/instagram.svg +5 -5
  7. package/dist/collection/components/weg-footer/icons/linkedin.svg +3 -3
  8. package/dist/collection/components/weg-footer/icons/tiktok.svg +3 -3
  9. package/dist/collection/components/weg-footer/icons/youtube.svg +4 -4
  10. package/dist/collection/components/weg-footer/weg-footer.css +270 -201
  11. package/dist/collection/components/weg-footer/weg-footer.js +22 -26
  12. package/dist/collection/components/weg-header/weg-header.css +474 -365
  13. package/dist/collection/components/weg-header/weg-header.js +226 -166
  14. package/dist/collection/constants/breakpoints.js +6 -0
  15. package/dist/collection/styles/shared.css +72 -78
  16. package/dist/collection/utils/layout.js +57 -9
  17. package/dist/components/p-BnMIy-A5.js +1 -0
  18. package/dist/components/weg-footer.js +1 -1
  19. package/dist/components/weg-header.js +1 -1
  20. package/dist/esm/loader.js +1 -1
  21. package/dist/esm/weg-footer_2.entry.js +307 -219
  22. package/dist/esm/weg-shared-layout.js +1 -1
  23. package/dist/types/components/weg-footer/weg-footer.d.ts +2 -2
  24. package/dist/types/components/weg-header/weg-header.d.ts +32 -16
  25. package/dist/types/components.d.ts +6 -48
  26. package/dist/types/constants/breakpoints.d.ts +6 -0
  27. package/dist/types/types/layout-data.d.ts +13 -27
  28. package/dist/types/utils/layout.d.ts +9 -1
  29. package/dist/weg-shared-layout/p-d74a58cf.entry.js +1 -0
  30. package/dist/weg-shared-layout/weg-shared-layout.esm.js +1 -1
  31. package/docs/angular-integration-guide.md +1004 -0
  32. package/docs/angular.md +229 -138
  33. package/docs/nextjs.md +306 -305
  34. package/docs/publishing.md +48 -48
  35. package/docs/react.md +261 -261
  36. package/docs/vanilla.md +138 -140
  37. package/package.json +12 -8
  38. package/readme.md +82 -107
  39. package/src/assets/dummy-data.json +137 -63
  40. package/src/constants/breakpoints.ts +7 -0
  41. package/dist/cjs/my-component.cjs.entry.js +0 -33
  42. package/dist/collection/components/my-component/my-component.css +0 -3
  43. package/dist/collection/components/my-component/my-component.js +0 -95
  44. package/dist/collection/components/weg-header/signed-in-layout.js +0 -13
  45. package/dist/collection/constants/weg-urls.js +0 -18
  46. package/dist/components/my-component.d.ts +0 -11
  47. package/dist/components/my-component.js +0 -1
  48. package/dist/components/p-DbIEJ3IT.js +0 -1
  49. package/dist/esm/my-component.entry.js +0 -31
  50. package/dist/types/components/my-component/my-component.d.ts +0 -16
  51. package/dist/types/components/weg-header/signed-in-layout.d.ts +0 -18
  52. package/dist/types/constants/weg-urls.d.ts +0 -18
  53. package/dist/weg-shared-layout/p-80fa529c.entry.js +0 -1
  54. package/dist/weg-shared-layout/p-d61033bd.entry.js +0 -1
  55. package/src/constants/weg-urls.ts +0 -23
  56. /package/dist/types/Users/{tan/Documents/code → rakin/Documents/WEG/repos}/weg-shared-layout/.stencil/test-utils/axe.d.ts +0 -0
  57. /package/dist/types/Users/{tan/Documents/code → rakin/Documents/WEG/repos}/weg-shared-layout/.stencil/test-utils/viewport.d.ts +0 -0
@@ -0,0 +1,1004 @@
1
+ # Angular integration guide
2
+
3
+ Step-by-step guide for **Angular 16+** (standalone or NgModule). Covers static fixtures, HTTP layout, auth, and common pitfalls.
4
+
5
+ | Doc | When to use it |
6
+ | --- | --- |
7
+ | **This guide** | First integration, production setup, API mapping, testing, SSR |
8
+ | **[angular.md](./angular.md)** | Short copy-paste quick start (same steps, fewer words) |
9
+ | **[weg-angular-demo](https://github.com/jobsac/weg-angular-demo)** | Runnable reference app |
10
+
11
+ ---
12
+
13
+ ## How to read this guide
14
+
15
+ 1. Follow **[Step-by-step integration](#step-by-step-integration)** in order — that is the complete happy path.
16
+ 2. Skim **[Layout data reference](#layout-data-reference)** when shaping CMS/API JSON.
17
+ 3. Use **[Optional sections](#optional-sections)** only when they match your app (separate API arrays, signals, SSR, tests, etc.).
18
+ 4. If something breaks, go to **[Troubleshooting](#troubleshooting)**.
19
+
20
+ **Steps 6 and 7 include two variants each** — pick the one that matches your app:
21
+
22
+ | Variant | Angular | State style |
23
+ | --- | --- | --- |
24
+ | **6A / 7A** | 16+ | Plain fields + RxJS `async` pipe (HTTP) |
25
+ | **6B / 7B** | 16.1+ (signals) | `signal()` + `toSignal()` for HTTP |
26
+
27
+ The web components are identical; only Angular template syntax differs (`layoutData` vs `layoutData()`).
28
+
29
+ ---
30
+
31
+ ## Integration checklist
32
+
33
+ Copy this list while integrating:
34
+
35
+ - [ ] **Step 1** — `npm i weg-shared-layout`
36
+ - [ ] **Step 2** — `defineCustomElements()` in `main.ts` **before** bootstrap
37
+ - [ ] **Step 3** — `resolveJsonModule` in `tsconfig.app.json` (if importing JSON)
38
+ - [ ] **Step 4** — `src/app/auth.ts` with sign-in URL constants (host app, not from package)
39
+ - [ ] **Step 5** — `CUSTOM_ELEMENTS_SCHEMA` on the shell `@Component` (or `@NgModule`) that owns `<weg-header>` / `<weg-footer>`
40
+ - [ ] **Step 6** — Shell template with **`[layout]`** property binding (not `layout="..."`)
41
+ - [ ] **Step 7** — Replace fixture with HTTP/CMS when ready; keep `{ header, footer }` object shape
42
+ - [ ] **Step 8** — Header: `[signedIn]`, `[userName]`, `(wegAuthClick)` + `preventDefault()` if you handle auth
43
+ - [ ] **Step 9** — Confirm in DevTools: `$0.layout` is an object on `<weg-header>`
44
+
45
+ ---
46
+
47
+ ## What you are integrating
48
+
49
+ `weg-shared-layout` ships two **Stencil Web Components**:
50
+
51
+ | Tag | Purpose |
52
+ | --- | --- |
53
+ | `<weg-header>` | Header — bundled logo, CMS nav (signed out), built-in nav (signed in), Sign in / Manage Account / Sign out |
54
+ | `<weg-footer>` | Footer — social links, menu, credits, copyright |
55
+
56
+ They are **presentational**: they **do not** fetch data. Your Angular app loads layout JSON and passes it in.
57
+
58
+ Angular treats them as **custom elements**. Bind JavaScript **properties** (square brackets in templates):
59
+
60
+ ```js
61
+ header.layout = { header: { ... }, footer: { ... } };
62
+ header.signedIn = true;
63
+ header.userName = 'Alex';
64
+ ```
65
+
66
+ The components do not depend on signals, RxJS, or standalone vs NgModule — only that values reach the element properties.
67
+
68
+ ### Requirements
69
+
70
+ | Requirement | Notes |
71
+ | --- | --- |
72
+ | **Angular 16+** | Standalone and NgModule both work |
73
+ | **Property binding** | `[layout]="obj"` — not `layout="{{ obj }}"` |
74
+ | **`CUSTOM_ELEMENTS_SCHEMA`** | On every component template that uses the tags; **does not** cascade to routed children |
75
+ | **One-time registration** | `defineCustomElements()` once per browser load (recommended) |
76
+
77
+ ---
78
+
79
+ ## Step-by-step integration
80
+
81
+ These steps assume a **standalone** Angular app with `src/app/app.ts` + `src/app/app.html`. For **NgModule** bootstrap, see [Optional: NgModule bootstrap](#optional-ngmodule-bootstrap).
82
+
83
+ ### Step 1 — Install the package
84
+
85
+ ```bash
86
+ npm i weg-shared-layout
87
+ # or
88
+ pnpm add weg-shared-layout
89
+ ```
90
+
91
+ ---
92
+
93
+ ### Step 2 — Register custom elements (once, before bootstrap)
94
+
95
+ **Recommended:** call the Stencil loader in `src/main.ts` **before** `bootstrapApplication`:
96
+
97
+ ```ts
98
+ // src/main.ts
99
+ import { bootstrapApplication } from '@angular/platform-browser';
100
+ import { defineCustomElements } from 'weg-shared-layout/loader';
101
+
102
+ import { appConfig } from './app/app.config';
103
+ import { App } from './app/app';
104
+
105
+ defineCustomElements();
106
+
107
+ bootstrapApplication(App, appConfig).catch((err) => console.error(err));
108
+ ```
109
+
110
+ **Alternative:** side-effect import only the tags you use (also once, before bootstrap). Pick one approach, not both:
111
+
112
+ ```ts
113
+ import 'weg-shared-layout/weg-header';
114
+ import 'weg-shared-layout/weg-footer';
115
+ ```
116
+
117
+ If header/footer are empty but tags appear in the DOM, registration did not run or ran too late.
118
+
119
+ ---
120
+
121
+ ### Step 3 — TypeScript: import layout JSON
122
+
123
+ In `tsconfig.app.json` (or `tsconfig.json`):
124
+
125
+ ```json
126
+ {
127
+ "compilerOptions": {
128
+ "resolveJsonModule": true
129
+ }
130
+ }
131
+ ```
132
+
133
+ Create a shared layout type in `src/app/layout.types.ts`:
134
+
135
+ ```ts
136
+ // src/app/layout.types.ts
137
+ import layoutFixture from 'weg-shared-layout/dummy-data.json';
138
+
139
+ /** Aligns with weg-shared-layout / CMS GET /api/layout shape */
140
+ export type LayoutData = typeof layoutFixture;
141
+ ```
142
+
143
+ During development you can import the package fixture:
144
+
145
+ ```ts
146
+ import layoutFixture from 'weg-shared-layout/dummy-data.json';
147
+ ```
148
+
149
+ Production: same shape from your API (e.g. [`GET /api/layout`](https://weg-payload-test.vercel.app/api/layout)).
150
+
151
+ ---
152
+
153
+ ### Step 4 — Auth URLs in your app (not from the package)
154
+
155
+ Create `src/app/auth.ts`. Host apps own sign-in/out URLs — do not import auth constants from `weg-shared-layout`.
156
+
157
+ ```ts
158
+ // src/app/auth.ts
159
+ export const HEADER_SIGN_IN = {
160
+ label: 'Sign in',
161
+ href: 'https://account.warwickemploymentgroup.com/account/login',
162
+ };
163
+
164
+ /** Where to send the user after sign-out (often same as login) */
165
+ export const ACCOUNT_LOGIN_HREF = HEADER_SIGN_IN.href;
166
+ ```
167
+
168
+ If your API omits a Sign in entry in `header.menu`, append `HEADER_SIGN_IN` when building layout — see Step 7.
169
+
170
+ ---
171
+
172
+ ### Step 5 — Allow custom elements in templates
173
+
174
+ Angular's compiler does not recognize `<weg-header>` / `<weg-footer>` unless you opt in.
175
+
176
+ Add `schemas: [CUSTOM_ELEMENTS_SCHEMA]` to the **shell** component whose template contains the tags (usually `App`):
177
+
178
+ ```ts
179
+ // src/app/app.ts
180
+ import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
181
+ import { RouterOutlet } from '@angular/router';
182
+
183
+ @Component({
184
+ selector: 'app-root',
185
+ imports: [RouterOutlet],
186
+ schemas: [CUSTOM_ELEMENTS_SCHEMA],
187
+ templateUrl: './app.html',
188
+ })
189
+ export class App {
190
+ // layout + auth — Step 6
191
+ }
192
+ ```
193
+
194
+ **Important:** If only `App` has the header/footer, only `App` needs the schema. Routed child components do **not** inherit it from a parent.
195
+
196
+ ---
197
+
198
+ ### Step 6 — Shell component with layout (static fixture)
199
+
200
+ Complete minimal shell (no HTTP yet). Use **6A** or **6B** — not both.
201
+
202
+ #### Step 6A — Plain properties (Angular 16+)
203
+
204
+ **`src/app/app.ts`:**
205
+
206
+ ```ts
207
+ import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
208
+ import { RouterOutlet } from '@angular/router';
209
+ import layoutFixture from 'weg-shared-layout/dummy-data.json';
210
+
211
+ import { ACCOUNT_LOGIN_HREF, HEADER_SIGN_IN } from './auth';
212
+ import type { LayoutData } from './layout.types';
213
+
214
+ @Component({
215
+ selector: 'app-root',
216
+ imports: [RouterOutlet],
217
+ schemas: [CUSTOM_ELEMENTS_SCHEMA],
218
+ templateUrl: './app.html',
219
+ })
220
+ export class App {
221
+ /** Same object for header and footer */
222
+ layoutData: LayoutData = layoutFixture;
223
+
224
+ signedIn = false;
225
+ userName?: string;
226
+
227
+ onAuthClick(event: Event): void {
228
+ const customEvent = event as CustomEvent<{ action: 'sign-in' | 'sign-out' }>;
229
+ customEvent.preventDefault();
230
+
231
+ if (customEvent.detail.action === 'sign-out') {
232
+ this.signedIn = false;
233
+ window.location.href = ACCOUNT_LOGIN_HREF;
234
+ return;
235
+ }
236
+
237
+ window.location.href = HEADER_SIGN_IN.href;
238
+ }
239
+ }
240
+ ```
241
+
242
+ **`src/app/app.html`:**
243
+
244
+ ```html
245
+ <weg-header
246
+ [layout]="layoutData"
247
+ [signedIn]="signedIn"
248
+ [userName]="userName"
249
+ (wegAuthClick)="onAuthClick($event)"
250
+ ></weg-header>
251
+
252
+ <main>
253
+ <router-outlet />
254
+ </main>
255
+
256
+ <weg-footer [layout]="layoutData"></weg-footer>
257
+ ```
258
+
259
+ Binding rules (6A):
260
+
261
+ | Correct | Wrong |
262
+ | --- | --- |
263
+ | `[layout]="layoutData"` | `layout="{{ layoutData }}"` (string attribute) |
264
+ | `[signedIn]="signedIn"` | Omitting `signedIn` when you need signed-in nav |
265
+ | No `()` on bindings | `layoutData()` — that is for **6B** only |
266
+
267
+ ---
268
+
269
+ #### Step 6B — Signals (Angular 16.1+)
270
+
271
+ Same shell as 6A; bindings use `()` because values are signals.
272
+
273
+ **`src/app/app.ts`:**
274
+
275
+ ```ts
276
+ import { Component, CUSTOM_ELEMENTS_SCHEMA, signal } from '@angular/core';
277
+ import { RouterOutlet } from '@angular/router';
278
+ import layoutFixture from 'weg-shared-layout/dummy-data.json';
279
+
280
+ import { ACCOUNT_LOGIN_HREF, HEADER_SIGN_IN } from './auth';
281
+ import type { LayoutData } from './layout.types';
282
+
283
+ @Component({
284
+ selector: 'app-root',
285
+ imports: [RouterOutlet],
286
+ schemas: [CUSTOM_ELEMENTS_SCHEMA],
287
+ templateUrl: './app.html',
288
+ })
289
+ export class App {
290
+ readonly layoutData = signal<LayoutData>(layoutFixture);
291
+
292
+ readonly signedIn = signal(false);
293
+ readonly userName = signal<string | undefined>(undefined);
294
+
295
+ onAuthClick(event: Event): void {
296
+ const customEvent = event as CustomEvent<{ action: 'sign-in' | 'sign-out' }>;
297
+ customEvent.preventDefault();
298
+
299
+ if (customEvent.detail.action === 'sign-out') {
300
+ this.signedIn.set(false);
301
+ this.userName.set(undefined);
302
+ window.location.href = ACCOUNT_LOGIN_HREF;
303
+ return;
304
+ }
305
+
306
+ window.location.href = HEADER_SIGN_IN.href;
307
+ }
308
+ }
309
+ ```
310
+
311
+ **`src/app/app.html`:**
312
+
313
+ ```html
314
+ <weg-header
315
+ [layout]="layoutData()"
316
+ [signedIn]="signedIn()"
317
+ [userName]="userName()"
318
+ (wegAuthClick)="onAuthClick($event)"
319
+ ></weg-header>
320
+
321
+ <main>
322
+ <router-outlet />
323
+ </main>
324
+
325
+ <weg-footer [layout]="layoutData()"></weg-footer>
326
+ ```
327
+
328
+ Run the app — you should see header nav and footer. Continue to **Step 7A** or **7B** for HTTP.
329
+
330
+ ---
331
+
332
+ ### Step 7 — Load layout from your API
333
+
334
+ Use **7A** (RxJS `async` pipe) or **7B** (`toSignal`) — same `LayoutService` as below. Pick the pair that matches Step 6 (6A → 7A, 6B → 7B).
335
+
336
+ **`src/app/layout.service.ts`** (shared by 7A and 7B):
337
+
338
+ ```ts
339
+ import { Injectable } from '@angular/core';
340
+ import { HttpClient } from '@angular/common/http';
341
+ import { map, Observable } from 'rxjs';
342
+
343
+ import { HEADER_SIGN_IN } from './auth';
344
+ import type { LayoutData } from './layout.types';
345
+
346
+ @Injectable({ providedIn: 'root' })
347
+ export class LayoutService {
348
+ constructor(private http: HttpClient) {}
349
+
350
+ /** CMS returns full { header, footer } object */
351
+ loadLayout(): Observable<LayoutData> {
352
+ return this.http.get<LayoutData>('/api/layout').pipe(
353
+ map((data) => this.ensureSignInMenu(data)),
354
+ );
355
+ }
356
+
357
+ private ensureSignInMenu(data: LayoutData): LayoutData {
358
+ const menu = data.header?.menu ?? [];
359
+ const hasSignIn = menu.some(
360
+ (item) =>
361
+ !item.items?.length &&
362
+ item.label?.trim().toLowerCase() === 'sign in',
363
+ );
364
+ if (hasSignIn) return data;
365
+ return {
366
+ ...data,
367
+ header: { ...data.header, menu: [...menu, HEADER_SIGN_IN] },
368
+ };
369
+ }
370
+ }
371
+ ```
372
+
373
+ Register `provideHttpClient()` in `app.config.ts` if not already present.
374
+
375
+ #### Step 7A — HTTP with RxJS + `async` pipe (pairs with 6A)
376
+
377
+ **`src/app/app.ts`:**
378
+
379
+ ```ts
380
+ import { AsyncPipe } from '@angular/common';
381
+ import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
382
+ import { RouterOutlet } from '@angular/router';
383
+ import { Observable } from 'rxjs';
384
+
385
+ import { ACCOUNT_LOGIN_HREF, HEADER_SIGN_IN } from './auth';
386
+ import type { LayoutData } from './layout.types';
387
+ import { LayoutService } from './layout.service';
388
+
389
+ @Component({
390
+ selector: 'app-root',
391
+ imports: [AsyncPipe, RouterOutlet],
392
+ schemas: [CUSTOM_ELEMENTS_SCHEMA],
393
+ templateUrl: './app.html',
394
+ })
395
+ export class App {
396
+ layout$: Observable<LayoutData>;
397
+ signedIn = false;
398
+ userName?: string;
399
+
400
+ constructor(layoutService: LayoutService) {
401
+ this.layout$ = layoutService.loadLayout();
402
+ }
403
+
404
+ onAuthClick(event: Event): void {
405
+ const customEvent = event as CustomEvent<{ action: 'sign-in' | 'sign-out' }>;
406
+ customEvent.preventDefault();
407
+
408
+ if (customEvent.detail.action === 'sign-out') {
409
+ this.signedIn = false;
410
+ window.location.href = ACCOUNT_LOGIN_HREF;
411
+ return;
412
+ }
413
+
414
+ window.location.href = HEADER_SIGN_IN.href;
415
+ }
416
+ }
417
+ ```
418
+
419
+ **`src/app/app.html`:**
420
+
421
+ ```html
422
+ @if (layout$ | async; as layout) {
423
+ <weg-header
424
+ [layout]="layout"
425
+ [signedIn]="signedIn"
426
+ [userName]="userName"
427
+ (wegAuthClick)="onAuthClick($event)"
428
+ ></weg-header>
429
+
430
+ <main>
431
+ <router-outlet />
432
+ </main>
433
+
434
+ <weg-footer [layout]="layout"></weg-footer>
435
+ }
436
+ ```
437
+
438
+ `@if (layout$ | async; as layout)` avoids a flash of empty header while the request is in flight. On older Angular without `@if`, use `*ngIf="layout$ | async as layout"`.
439
+
440
+ Use `shareReplay(1)` on `layout$` if multiple subscribers need the same payload.
441
+
442
+ ---
443
+
444
+ #### Step 7B — HTTP with signals + `toSignal` (pairs with 6B)
445
+
446
+ Requires **Angular 16.1+** (`@angular/core/rxjs-interop`).
447
+
448
+ **`src/app/app.ts`:**
449
+
450
+ ```ts
451
+ import { Component, CUSTOM_ELEMENTS_SCHEMA, inject, signal } from '@angular/core';
452
+ import { toSignal } from '@angular/core/rxjs-interop';
453
+ import { RouterOutlet } from '@angular/router';
454
+
455
+ import { ACCOUNT_LOGIN_HREF, HEADER_SIGN_IN } from './auth';
456
+ import { LayoutService } from './layout.service';
457
+
458
+ @Component({
459
+ selector: 'app-root',
460
+ imports: [RouterOutlet],
461
+ schemas: [CUSTOM_ELEMENTS_SCHEMA],
462
+ templateUrl: './app.html',
463
+ })
464
+ export class App {
465
+ private readonly layoutService = inject(LayoutService);
466
+
467
+ readonly layout = toSignal(this.layoutService.loadLayout());
468
+ readonly signedIn = signal(false);
469
+ readonly userName = signal<string | undefined>(undefined);
470
+
471
+ onAuthClick(event: Event): void {
472
+ const customEvent = event as CustomEvent<{ action: 'sign-in' | 'sign-out' }>;
473
+ customEvent.preventDefault();
474
+
475
+ if (customEvent.detail.action === 'sign-out') {
476
+ this.signedIn.set(false);
477
+ this.userName.set(undefined);
478
+ window.location.href = ACCOUNT_LOGIN_HREF;
479
+ return;
480
+ }
481
+
482
+ window.location.href = HEADER_SIGN_IN.href;
483
+ }
484
+ }
485
+ ```
486
+
487
+ **`src/app/app.html`:**
488
+
489
+ ```html
490
+ @if (layout(); as layout) {
491
+ <weg-header
492
+ [layout]="layout"
493
+ [signedIn]="signedIn()"
494
+ [userName]="userName()"
495
+ (wegAuthClick)="onAuthClick($event)"
496
+ ></weg-header>
497
+
498
+ <main>
499
+ <router-outlet />
500
+ </main>
501
+
502
+ <weg-footer [layout]="layout"></weg-footer>
503
+ }
504
+ ```
505
+
506
+ `toSignal` bridges the HTTP `Observable` from `LayoutService` into a signal for the template. Session fields (`signedIn`, `userName`) stay as signals, same as Step 6B.
507
+
508
+ ---
509
+
510
+ ### Step 8 — Header auth behaviour
511
+
512
+ | Binding | Web component | Purpose |
513
+ | --- | --- | --- |
514
+ | `[layout]="..."` | `layout` | CMS nav when **signed out** |
515
+ | `[signedIn]="..."` | `signed-in` | `true` → built-in signed-in nav (ignores CMS `header.menu`) |
516
+ | `[userName]="..."` | `user-name` | First name on Manage Account when signed in |
517
+ | `(wegAuthClick)="..."` | `wegAuthClick` | `detail.action`: `'sign-in'` \| `'sign-out'` |
518
+
519
+ **Signed out** (`signedIn === false`):
520
+
521
+ - Uses `layout.header.menu` — dropdown groups (`items`) and flat links (`href`), including Sign in
522
+ - Logo **image** from `layout.header.logoSrc` (bundled if omitted); logo **link** always goes to WEG home
523
+
524
+ **Signed in** (`signedIn === true`):
525
+
526
+ - Built-in links: Find a job, Dashboard, Manage Account, Sign out
527
+ - `userName` customizes Manage Account label
528
+
529
+ Call `event.preventDefault()` in `onAuthClick` when **your** app handles navigation. Without `preventDefault()`, the component uses default redirects.
530
+
531
+ Wire `signedIn` / `userName` from your session service (set `signedIn = true` and `userName` when the user logs in).
532
+
533
+ ---
534
+
535
+ ### Step 9 — Verify in the browser
536
+
537
+ 1. Open DevTools → Elements → select `<weg-header>`.
538
+ 2. In the console:
539
+
540
+ ```js
541
+ $0.layout // must be a plain object, not undefined
542
+ $0.signedIn // boolean
543
+ $0.userName // string or undefined
544
+ ```
545
+
546
+ 3. If `layout` is missing or looks like `"[object Object]"`, you used attribute binding instead of `[layout]`.
547
+ 4. Footer: same `layout` object; only `layout.footer` is read.
548
+
549
+ ---
550
+
551
+ ## Layout data reference
552
+
553
+ Both tags accept one **`layout`** object with optional `header` and `footer` keys — **not** a bare array at the top level.
554
+
555
+ ### TypeScript shape
556
+
557
+ ```ts
558
+ type LayoutLink = {
559
+ label: string;
560
+ href?: string;
561
+ target?: '_blank' | '_self' | '_parent' | '_top';
562
+ items?: LayoutLink[];
563
+ };
564
+
565
+ type LayoutHeaderData = {
566
+ logoSrc?: string;
567
+ menu?: LayoutLink[];
568
+ };
569
+
570
+ type LayoutFooterSocialLink = {
571
+ platform: 'LinkedIn' | 'Instagram' | 'TikTok' | 'YouTube';
572
+ href: string;
573
+ };
574
+
575
+ type LayoutFooterData = {
576
+ social?: LayoutFooterSocialLink[];
577
+ menu?: LayoutLink[][];
578
+ credits?: string;
579
+ copyright?: string;
580
+ };
581
+
582
+ type LayoutData = {
583
+ header?: Partial<LayoutHeaderData>;
584
+ footer?: Partial<LayoutFooterData>;
585
+ };
586
+ ```
587
+
588
+ Full example JSON:
589
+
590
+ - **From npm:** `import layout from 'weg-shared-layout/dummy-data.json'`
591
+ - **In this repo:** [`src/assets/dummy-data.json`](../src/assets/dummy-data.json)
592
+ - **Live CMS sample:** [`GET /api/layout`](https://weg-payload-test.vercel.app/api/layout)
593
+
594
+ ### Abbreviated JSON
595
+
596
+ ```json
597
+ {
598
+ "header": {
599
+ "logoSrc": "https://www.warwickemploymentgroup.com/assets/weg-logo.svg",
600
+ "menu": [
601
+ {
602
+ "label": "Find a job",
603
+ "items": [
604
+ { "label": "Graduates", "href": "https://example.com/jobs/graduates" }
605
+ ]
606
+ },
607
+ { "label": "Career advice", "href": "https://example.com/career-advice" },
608
+ { "label": "Register", "href": "https://example.com/register" },
609
+ { "label": "Sign in", "href": "https://example.com/login" }
610
+ ]
611
+ },
612
+ "footer": {
613
+ "social": [
614
+ { "platform": "LinkedIn", "href": "https://linkedin.com/company/example" }
615
+ ],
616
+ "menu": [
617
+ [{ "label": "About WEG", "href": "/about" }]
618
+ ],
619
+ "credits": "Warwick Employment Group …",
620
+ "copyright": "Copyright © Warwick Employment Group."
621
+ }
622
+ }
623
+ ```
624
+
625
+ ### Which fields are used when
626
+
627
+ | Field | Used when | Notes |
628
+ | --- | --- | --- |
629
+ | `header.logoSrc` | Signed **out** | Logo image URL; bundled if omitted |
630
+ | `header.menu` | Signed **out** | Unified nav — groups use `items[]`, flat links use `href` |
631
+ | `footer.social` | Always | `platform` must be `LinkedIn`, `Instagram`, `TikTok`, or `YouTube` exactly |
632
+ | `footer.menu` | Always | `LayoutLink[][]` — one link array per column |
633
+ | `footer.credits` | Always | Text below menu |
634
+ | `footer.copyright` | Always | Copyright line |
635
+
636
+ When **`signedIn` is true**, `<weg-header>` **ignores** `header.menu` from layout.
637
+
638
+ ### Property binding vs attributes
639
+
640
+ ```html
641
+ <!-- Correct -->
642
+ <weg-header [layout]="layoutData"></weg-header>
643
+
644
+ <!-- Wrong for objects -->
645
+ <weg-header layout="{{ layoutData }}"></weg-header>
646
+ ```
647
+
648
+ The component can parse a JSON **string** for `layout`, but `[layout]="object"` is the Angular norm.
649
+
650
+ ### Invalid entries are silently dropped
651
+
652
+ - Links without `label` or `href` are skipped
653
+ - Menu groups with empty `items` are skipped
654
+ - Unknown social `platform` values are skipped
655
+ - Malformed input → empty sections (no throw) — inspect `$0.layout` in DevTools
656
+
657
+ ---
658
+
659
+ ## Optional sections
660
+
661
+ Read only what applies to your app.
662
+
663
+ ### Optional: API returns separate arrays (not one layout object)
664
+
665
+ **Skip this section** if your API already returns `{ header: { ... }, footer: { ... } }`.
666
+
667
+ The top-level `[layout]` binding must be an **object**, not an array:
668
+
669
+ ```ts
670
+ // ❌ Will NOT work — nav stays empty
671
+ const navLinks = [
672
+ { label: 'Career advice', href: '/career-advice' },
673
+ ];
674
+ // template: [layout]="navLinks"
675
+ ```
676
+
677
+ **✅** Put arrays **inside** the layout object:
678
+
679
+ | Field | Array element shape |
680
+ | --- | --- |
681
+ | `header.menu` | `LayoutLink` — group with `items[]` or flat link with `href` |
682
+ | `footer.social` | `{ platform, href }` |
683
+ | `footer.menu` | `{ label, href }[]` per column |
684
+
685
+ **`src/app/layout.service.ts` — merge multiple endpoints into one menu:**
686
+
687
+ ```ts
688
+ import { Injectable } from '@angular/core';
689
+ import { HttpClient } from '@angular/common/http';
690
+ import { forkJoin, map, Observable } from 'rxjs';
691
+
692
+ import { HEADER_SIGN_IN } from './auth';
693
+ import type { LayoutData } from './layout.types';
694
+
695
+ @Injectable({ providedIn: 'root' })
696
+ export class LayoutService {
697
+ constructor(private http: HttpClient) {}
698
+
699
+ loadLayoutFromParts(): Observable<LayoutData> {
700
+ return forkJoin({
701
+ menuGroups: this.http.get<NonNullable<LayoutData['header']>['menu']>(
702
+ '/api/nav/menu',
703
+ ),
704
+ flatLinks: this.http.get<NonNullable<LayoutData['header']>['menu']>(
705
+ '/api/nav/links',
706
+ ),
707
+ footer: this.http.get<LayoutData['footer']>('/api/footer'),
708
+ }).pipe(
709
+ map(({ menuGroups, flatLinks, footer }) => ({
710
+ header: {
711
+ menu: [...menuGroups, ...flatLinks, HEADER_SIGN_IN],
712
+ },
713
+ footer,
714
+ })),
715
+ );
716
+ }
717
+ }
718
+ ```
719
+
720
+ Bind the result the same way as Step 7.
721
+
722
+ ---
723
+
724
+ ### Optional: Plain vs signals (cheat sheet)
725
+
726
+ | | Step 6A / 7A (plain + RxJS) | Step 6B / 7B (signals) |
727
+ | --- | --- | --- |
728
+ | Angular | 16+ | 16.1+ |
729
+ | Static layout | `layoutData = { ... }` | `layoutData = signal({ ... })` |
730
+ | Template | `[layout]="layoutData"` | `[layout]="layoutData()"` |
731
+ | HTTP layout | `layout$` + `\| async` | `layout = toSignal(loadLayout())` |
732
+ | Update signed in | `this.signedIn = true` | `this.signedIn.set(true)` |
733
+
734
+ Full code for both variants: **Step 6** and **Step 7** above, and [angular.md §5](./angular.md#5-layout-shell--pick-one).
735
+
736
+ ---
737
+
738
+ ### Optional: Central layout service (BehaviorSubject)
739
+
740
+ Use when multiple components need the same layout or you update it imperatively:
741
+
742
+ ```ts
743
+ import { Injectable } from '@angular/core';
744
+ import { BehaviorSubject } from 'rxjs';
745
+ import layoutFixture from 'weg-shared-layout/dummy-data.json';
746
+
747
+ import { HEADER_SIGN_IN } from './auth';
748
+ import type { LayoutData } from './layout.types';
749
+
750
+ @Injectable({ providedIn: 'root' })
751
+ export class LayoutService {
752
+ private readonly layoutSubject = new BehaviorSubject<LayoutData>(layoutFixture);
753
+
754
+ readonly layout$ = this.layoutSubject.asObservable();
755
+
756
+ setLayout(data: LayoutData): void {
757
+ this.layoutSubject.next(this.ensureSignInMenu(data));
758
+ }
759
+
760
+ private ensureSignInMenu(data: LayoutData): LayoutData {
761
+ const menu = data.header?.menu ?? [];
762
+ const hasSignIn = menu.some(
763
+ (item) =>
764
+ !item.items?.length &&
765
+ item.label?.trim().toLowerCase() === 'sign in',
766
+ );
767
+ if (hasSignIn) return data;
768
+ return {
769
+ ...data,
770
+ header: { ...data.header, menu: [...menu, HEADER_SIGN_IN] },
771
+ };
772
+ }
773
+ }
774
+ ```
775
+
776
+ ---
777
+
778
+ ### Optional: Update layout at runtime
779
+
780
+ Stencil watches the `layout` **property**. Assign a **new object reference** when data changes:
781
+
782
+ ```ts
783
+ // Good
784
+ this.layoutData = {
785
+ ...this.layoutData,
786
+ header: { ...this.layoutData.header, menu: newMenu },
787
+ };
788
+
789
+ // Risky — may not re-render
790
+ this.layoutData.header!.menu!.push(newLink);
791
+ ```
792
+
793
+ With `OnPush` and plain fields, call `ChangeDetectorRef.markForCheck()` after updates.
794
+
795
+ ---
796
+
797
+ ### Optional: Imperative assignment (`@ViewChild`)
798
+
799
+ Rare — use when property binding is blocked (legacy wrapper):
800
+
801
+ ```ts
802
+ import {
803
+ AfterViewInit,
804
+ Component,
805
+ CUSTOM_ELEMENTS_SCHEMA,
806
+ ElementRef,
807
+ ViewChild,
808
+ } from '@angular/core';
809
+ import layoutFixture from 'weg-shared-layout/dummy-data.json';
810
+ import type { LayoutData } from './layout.types';
811
+
812
+ @Component({
813
+ schemas: [CUSTOM_ELEMENTS_SCHEMA],
814
+ template: `
815
+ <weg-header #header></weg-header>
816
+ <weg-footer #footer></weg-footer>
817
+ `,
818
+ })
819
+ export class ShellComponent implements AfterViewInit {
820
+ @ViewChild('header', { read: ElementRef }) headerRef!: ElementRef<HTMLElement>;
821
+ @ViewChild('footer', { read: ElementRef }) footerRef!: ElementRef<HTMLElement>;
822
+
823
+ layoutData: LayoutData = layoutFixture;
824
+
825
+ ngAfterViewInit(): void {
826
+ const header = this.headerRef.nativeElement as HTMLElement & {
827
+ layout?: LayoutData;
828
+ };
829
+ header.layout = this.layoutData;
830
+ (this.footerRef.nativeElement as HTMLElement & { layout?: LayoutData }).layout =
831
+ this.layoutData;
832
+ }
833
+ }
834
+ ```
835
+
836
+ ---
837
+
838
+ ### Optional: NgModule bootstrap
839
+
840
+ **`src/main.ts`:**
841
+
842
+ ```ts
843
+ import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
844
+ import { defineCustomElements } from 'weg-shared-layout/loader';
845
+
846
+ import { AppModule } from './app/app.module';
847
+
848
+ defineCustomElements();
849
+
850
+ platformBrowserDynamic()
851
+ .bootstrapModule(AppModule)
852
+ .catch((err) => console.error(err));
853
+ ```
854
+
855
+ **`src/app/app.module.ts`:**
856
+
857
+ ```ts
858
+ import { NgModule, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
859
+ import { BrowserModule } from '@angular/platform-browser';
860
+
861
+ import { App } from './app';
862
+
863
+ @NgModule({
864
+ declarations: [App],
865
+ imports: [BrowserModule],
866
+ schemas: [CUSTOM_ELEMENTS_SCHEMA],
867
+ bootstrap: [App],
868
+ })
869
+ export class AppModule {}
870
+ ```
871
+
872
+ Steps 4–9 are the same; declare the shell component in `declarations` instead of `imports: [App]`.
873
+
874
+ ---
875
+
876
+ ### Optional: Testing
877
+
878
+ In test setup:
879
+
880
+ ```ts
881
+ import { defineCustomElements } from 'weg-shared-layout/loader';
882
+
883
+ beforeAll(() => {
884
+ defineCustomElements();
885
+ });
886
+ ```
887
+
888
+ Before assertions:
889
+
890
+ ```ts
891
+ await Promise.all([
892
+ customElements.whenDefined('weg-header'),
893
+ customElements.whenDefined('weg-footer'),
894
+ ]);
895
+
896
+ fixture.detectChanges();
897
+ await fixture.whenStable();
898
+
899
+ const header = fixture.nativeElement.querySelector('weg-header') as HTMLElement & {
900
+ layout?: LayoutData;
901
+ };
902
+
903
+ expect(
904
+ header.layout?.header?.menu?.some(
905
+ (item) => item.label === 'Sign in' && item.href === HEADER_SIGN_IN.href,
906
+ ),
907
+ ).toBe(true);
908
+ ```
909
+
910
+ Full example: [weg-angular-demo `app.spec.ts`](https://github.com/jobsac/weg-angular-demo/blob/main/src/app/app.spec.ts).
911
+
912
+ ---
913
+
914
+ ### Optional: Server-side rendering (SSR)
915
+
916
+ Guard registration in `main.ts`:
917
+
918
+ ```ts
919
+ import { defineCustomElements } from 'weg-shared-layout/loader';
920
+
921
+ if (typeof window !== 'undefined') {
922
+ defineCustomElements();
923
+ }
924
+ ```
925
+
926
+ Layout JSON can be fetched on the server; custom elements render after client hydration.
927
+
928
+ ---
929
+
930
+ ## Footer
931
+
932
+ `<weg-footer>` only needs `[layout]` with the same object as the header:
933
+
934
+ ```html
935
+ <weg-footer [layout]="layoutData"></weg-footer>
936
+ ```
937
+
938
+ External `https://` links open in a new tab automatically.
939
+
940
+ ---
941
+
942
+ ## Troubleshooting
943
+
944
+ | Symptom | Likely cause | Fix |
945
+ | --- | --- | --- |
946
+ | `'weg-header' is not a known element` | Missing schema | `CUSTOM_ELEMENTS_SCHEMA` on the component that owns the template |
947
+ | Header/footer completely empty | Not registered | `defineCustomElements()` before bootstrap, or tag side-effect imports |
948
+ | Tags render, nav empty | Wrong `layout` | `[layout]="..."` with object `{ header?, footer? }` |
949
+ | Passed array to `[layout]` | Top-level must be object | `{ header: { menu: myArray } }` — see [Optional: API returns separate arrays](#optional-api-returns-separate-arrays-not-one-layout-object) |
950
+ | Auth always Sign in | `signedIn` not bound / false | Bind session; set `true` when logged in |
951
+ | Generic Manage Account | `userName` missing | Pass first name when `signedIn` is true |
952
+ | Layout changes ignored | In-place mutation | New object reference |
953
+ | Brief empty header with `async` | Observable not emitted yet | `@if (layout$ \| async; as layout)` or `BehaviorSubject` default |
954
+ | Tests fail | Elements undefined | `defineCustomElements()` in `beforeAll` |
955
+ | SSR `document is not defined` | Loader on server | `typeof window !== 'undefined'` guard |
956
+ | Missing social icons | Bad `platform` | Exactly `LinkedIn`, `Instagram`, `TikTok`, `YouTube` |
957
+ | Missing menu item | Normalization | Non-empty `label` and valid `items` or `href` |
958
+
959
+ ### DevTools
960
+
961
+ ```js
962
+ $0.layout
963
+ $0.signedIn
964
+ $0.userName
965
+ ```
966
+
967
+ ---
968
+
969
+ ## Quick reference
970
+
971
+ ### File map (standalone app)
972
+
973
+ | File | Role |
974
+ | --- | --- |
975
+ | `src/main.ts` | `defineCustomElements()` then bootstrap |
976
+ | `tsconfig.app.json` | `resolveJsonModule: true` |
977
+ | `src/app/auth.ts` | `HEADER_SIGN_IN`, `ACCOUNT_LOGIN_HREF` |
978
+ | `src/app/layout.types.ts` | `export type LayoutData = typeof layoutFixture` |
979
+ | `src/app/layout.service.ts` | HTTP / API mapping (Step 7, optional arrays) |
980
+ | `src/app/app.ts` | Shell: schema, layout, auth handler |
981
+ | `src/app/app.html` | `<weg-header>`, `<router-outlet>`, `<weg-footer>` |
982
+
983
+ ### Minimal template
984
+
985
+ ```html
986
+ <weg-header
987
+ [layout]="layoutData"
988
+ [signedIn]="signedIn"
989
+ [userName]="userName"
990
+ (wegAuthClick)="onAuthClick($event)"
991
+ ></weg-header>
992
+
993
+ <router-outlet></router-outlet>
994
+
995
+ <weg-footer [layout]="layoutData"></weg-footer>
996
+ ```
997
+
998
+ ### Related docs
999
+
1000
+ - [Quick start (angular.md)](./angular.md)
1001
+ - [Package readme](../readme.md)
1002
+ - [Plain HTML / vanilla JS](./vanilla.md)
1003
+ - [React SPA](./react.md)
1004
+ - [Reference demo app](https://github.com/jobsac/weg-angular-demo)