weg-shared-layout 0.0.54 → 0.0.58

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/docs/react.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # React (SPA)
2
2
 
3
- Guide for **client-rendered** React apps (Vite, Create React App, etc.). If you use **Next.js App Router**, see **[Next.js](./nextjs.md)** — SSR and Server Components require a different integration.
3
+ Guide for **client-rendered** React apps (Vite, Create React App, and similar). If you use **Next.js App Router**, see **[Next.js](./nextjs.md)** because SSR and Server Components need a different integration.
4
4
 
5
5
  ## Components
6
6
 
@@ -9,7 +9,7 @@ Guide for **client-rendered** React apps (Vite, Create React App, etc.). If you
9
9
  | `<weg-header>` | Site header — bundled logo, CMS nav (signed out), built-in nav (signed in), Sign in / Manage Account / Sign out |
10
10
  | `<weg-footer>` | Site footer — social links, menu, credits, copyright |
11
11
 
12
- Both are **presentational** [Stencil](https://stenciljs.com/) Web Components. They **do not fetch data** — your app passes a **`layout`** payload from the WEG CMS WEG21 API (via [`fetchWeg21Layout`](../src/utils/menus.ts)) or [`weg21-bootstrap.json`](../src/assets/weg21-bootstrap.json) and [`weg21-menus.json`](../src/assets/weg21-menus.json) as offline fallback.
12
+ Both are **presentational** [Stencil](https://stenciljs.com/) Web Components. They **do not fetch data**. Your app passes a **`layout`** payload from the WEG CMS WEG21 API (for example via [`fetchWeg21Layout`](../src/utils/menus.ts)) or from the bundled offline fixtures [`weg21-bootstrap.json`](../src/assets/weg21-bootstrap.json) and [`weg21-menus.json`](../src/assets/weg21-menus.json).
13
13
 
14
14
  `<weg-header>` additionally accepts **`signed-in`**, **`user-name`**, **`account-base-url`**, **`current-path`**, and emits **`wegAuthClick`**.
15
15
 
@@ -80,7 +80,7 @@ Sign-in is a normal `header.menu` link — add it in your CMS like Register. The
80
80
 
81
81
  ### Signed in
82
82
 
83
- Set **`signed-in`** and optionally **`user-name`**. The component **ignores CMS nav** and shows built-in links: Find a job, Dashboard, Manage Account, Sign out. Pass **`account-base-url`** when account links should use a non-production portal.
83
+ Set **`signed-in`** and optionally **`user-name`**. If `layout.header.menu` is present, the component keeps using that menu while switching to signed-in UI. If `layout.header.menu` is empty, it falls back to the built-in signed-in menu: Find a job, Dashboard, Manage Account, and Sign out. Pass **`account-base-url`** when those fallback account links should use a non-production portal.
84
84
 
85
85
  Handle **`wegAuthClick`** for sign-out (call your logout API, then redirect):
86
86
 
@@ -122,7 +122,7 @@ Without React Router, pass `window.location.pathname` (+ `search` when needed) o
122
122
 
123
123
  | Prop / event | Purpose |
124
124
  | --- | --- |
125
- | `signedIn={boolean}` | Switches to signed-in nav when `true` |
125
+ | `signedIn={boolean}` | Enables signed-in UI; the built-in signed-in fallback menu is used when `layout.header.menu` is empty |
126
126
  | `userName={string}` | First name beside profile icon on Manage Account |
127
127
  | `accountBaseUrl={string}` | Account portal origin for signed-in links (optional) |
128
128
  | `currentPath={string}` | Current path — highlights active nav (e.g. `/career-advice/foo`) |
@@ -180,23 +180,17 @@ const currentPath = search ? `${pathname}${search}` : pathname;
180
180
  import { useEffect, useState } from 'react';
181
181
  import 'weg-shared-layout/weg-header';
182
182
  import 'weg-shared-layout/weg-footer';
183
- import { dummyWeg21LayoutData } from 'weg-shared-layout/menus';
183
+ import { dummyWeg21LayoutData, fetchWeg21Layout } from 'weg-shared-layout/menus';
184
184
  const layoutFixture = dummyWeg21LayoutData();
185
185
 
186
186
  type LayoutData = typeof layoutFixture;
187
187
 
188
- const WEG21_API = 'https://warwickemploymentgroup.com/api/v1/weg21';
189
-
190
188
  export function SiteLayout({ children }: { children: React.ReactNode }) {
191
189
  const [layout, setLayout] = useState<LayoutData | null>(null);
192
190
 
193
191
  useEffect(() => {
194
192
  let cancelled = false;
195
- fetch(LAYOUT_URL)
196
- .then((res) => {
197
- if (!res.ok) throw new Error(`layout fetch failed: ${res.status}`);
198
- return res.json() as Promise<LayoutData>;
199
- })
193
+ fetchWeg21Layout({ apiKey: import.meta.env.VITE_WCMS_API_KEY })
200
194
  .then((data) => {
201
195
  if (!cancelled) setLayout(data);
202
196
  })
@@ -277,6 +271,7 @@ declare module 'react' {
277
271
  | Empty despite correct data | React set `layout` as attribute | React 19+, or `JSON.stringify`, or ref assignment. |
278
272
  | Logo missing on header | Old build without inlined logo | Upgrade package; logo is bundled in `logo-data.ts`. |
279
273
  | Auth always Sign in | `signed-in` not set | Bind `signedIn={!!session}`. |
274
+ | Signed-in links are not what you expected | `layout.header.menu` is still being used | Leave `layout.header.menu` empty if you want the built-in signed-in fallback nav. |
280
275
  | Manage Account shows label not name | `user-name` not set | Pass `userName` when signed in. |
281
276
  | Nav never highlights active page | `current-path` not set | Pass `currentPath` from your router (pathname + query when needed). |
282
277
  | `onWegAuthClick` not firing | React CE event binding | Use `addEventListener` on a ref. |
package/docs/vanilla.md CHANGED
@@ -7,7 +7,7 @@
7
7
  | `<weg-header>` | Site header — bundled logo, CMS nav (signed out), built-in nav (signed in), Sign in / Manage Account / Sign out |
8
8
  | `<weg-footer>` | Site footer — social links, menu, credits, copyright |
9
9
 
10
- Both components are **presentational**: they do **not** fetch data. Pass the same `layout` payload to each.
10
+ Both components are **presentational**: they do **not** fetch data. Pass the same `layout` payload to both.
11
11
 
12
12
  WEG21 API schema: [`weg21-bootstrap.json`](../src/assets/weg21-bootstrap.json), [`weg21-menus.json`](../src/assets/weg21-menus.json)
13
13
 
@@ -82,7 +82,7 @@ Renders `header.menu` from `layout`. Add sign-in as a normal link — `{ "label"
82
82
 
83
83
  ### Signed in
84
84
 
85
- Set **`signedIn`** and optionally **`userName`**. CMS nav is ignored; built-in links are shown (Find a job, Dashboard, Manage Account, Sign out). Set **`accountBaseUrl`** when account links should use a non-production portal.
85
+ Set **`signedIn`** and optionally **`userName`**. If `layout.header.menu` is present, the component keeps using that menu while switching to signed-in UI. If the menu is empty, the built-in signed-in fallback menu is shown instead: Find a job, Dashboard, Manage Account, and Sign out. Set **`accountBaseUrl`** when those fallback account links should use a non-production portal.
86
86
 
87
87
  ```js
88
88
  header.signedIn = true;
@@ -138,14 +138,21 @@ Logo **image** uses `header.logoSrc` (bundled if omitted). Logo **link** uses `h
138
138
 
139
139
  ## Without a bundler
140
140
 
141
- Fetch `weg21-bootstrap.json` and `weg21-menus.json`, map with the same logic as [`menusToLayoutData`](../src/utils/menus.ts), then assign properties:
141
+ If you are not using a bundler, you cannot import `weg-shared-layout/menus` directly from the package. In that case, fetch or embed a pre-mapped `layout` object yourself, or copy the same mapping logic as [`menusToLayoutData`](../src/utils/menus.ts) into your app.
142
+
143
+ At minimum, the object you assign still needs the same final shape:
142
144
 
143
145
  ```js
144
- const [bootstrapRes, menusRes] = await Promise.all([
145
- fetch('/assets/weg21-bootstrap.json'),
146
- fetch('/assets/weg21-menus.json'),
147
- ]);
148
- const layout = await res.json();
146
+ const layout = {
147
+ header: { menu: [...] },
148
+ footer: {
149
+ menu: [...],
150
+ social: [...],
151
+ credits: '...',
152
+ copyright: '...',
153
+ },
154
+ };
155
+
149
156
  document.getElementById('header').layout = layout;
150
157
  document.getElementById('footer').layout = layout;
151
158
  ```
package/hydrate/index.js CHANGED
@@ -5920,8 +5920,8 @@ class WegHeader {
5920
5920
  */
5921
5921
  userName;
5922
5922
  /**
5923
- * Account portal origin for sign-in, register, dashboard, and manage-account links.
5924
- * Defaults to production when omitted.
5923
+ * Account portal origin for the built-in signed-in fallback menu (when `header.menu` is empty).
5924
+ * Menu links from the CMS use their `href` as-is. Defaults to production when omitted.
5925
5925
  */
5926
5926
  accountBaseUrl;
5927
5927
  /**
@@ -6019,13 +6019,43 @@ class WegHeader {
6019
6019
  getMenuToggleButton() {
6020
6020
  return this.el.shadowRoot?.querySelector('.menu-toggle');
6021
6021
  }
6022
+ static FOCUSABLE_SELECTOR = 'a[href]:not([tabindex="-1"]), button:not([disabled]):not([tabindex="-1"])';
6022
6023
  getFirstNavFocusable() {
6023
6024
  const panel = this.el.shadowRoot?.querySelector('#weg-header-main-nav');
6024
6025
  if (!panel)
6025
6026
  return null;
6026
- const focusables = panel.querySelectorAll('a[href], button:not([disabled])');
6027
+ const focusables = panel.querySelectorAll(WegHeader.FOCUSABLE_SELECTOR);
6027
6028
  return focusables[0] ?? null;
6028
6029
  }
6030
+ /** Focusable controls inside the open mobile menu chrome (toggle → logo → auth → nav). */
6031
+ getMobileMenuFocusables() {
6032
+ const header = this.el.shadowRoot?.querySelector('.header');
6033
+ if (!header)
6034
+ return [];
6035
+ return Array.from(header.querySelectorAll(WegHeader.FOCUSABLE_SELECTOR));
6036
+ }
6037
+ trapMobileMenuFocus(event) {
6038
+ if (!this.isMobileMenuActive() || event.key !== 'Tab')
6039
+ return;
6040
+ const focusables = this.getMobileMenuFocusables();
6041
+ if (focusables.length === 0)
6042
+ return;
6043
+ const first = focusables[0];
6044
+ const last = focusables[focusables.length - 1];
6045
+ const active = this.el.shadowRoot?.activeElement;
6046
+ const activeInTrap = active != null && focusables.includes(active);
6047
+ if (event.shiftKey) {
6048
+ if (!activeInTrap || active === first) {
6049
+ event.preventDefault();
6050
+ last.focus();
6051
+ }
6052
+ return;
6053
+ }
6054
+ if (!activeInTrap || active === last) {
6055
+ event.preventDefault();
6056
+ first.focus();
6057
+ }
6058
+ }
6029
6059
  focusFirstNavItem() {
6030
6060
  setTimeout(() => {
6031
6061
  const attemptFocus = (retries = 8) => {
@@ -6137,6 +6167,10 @@ class WegHeader {
6137
6167
  this.resolve();
6138
6168
  }
6139
6169
  handleKeyDown(event) {
6170
+ if (event.key === 'Tab') {
6171
+ this.trapMobileMenuFocus(event);
6172
+ return;
6173
+ }
6140
6174
  if (event.key !== 'Escape')
6141
6175
  return;
6142
6176
  if (this.openDropdown) {
@@ -6396,6 +6430,20 @@ class WegHeader {
6396
6430
  const name = this.userName?.trim();
6397
6431
  return name || 'Manage Account';
6398
6432
  }
6433
+ resolveManageAccountHref(link) {
6434
+ const fromMenu = link?.href?.trim();
6435
+ if (fromMenu)
6436
+ return fromMenu;
6437
+ return accountPath(this.getAccountHome(), '/account/manage');
6438
+ }
6439
+ findManageAccountLink(menu) {
6440
+ const accountHome = this.getAccountHome();
6441
+ for (const item of menu) {
6442
+ if (!isMenuGroup(item) && isManageAccountLink(item, accountHome))
6443
+ return item;
6444
+ }
6445
+ return null;
6446
+ }
6399
6447
  shouldExcludeFromNavActive(item, accountHome) {
6400
6448
  return isSignOutLink(item) || isSignInLink(item, accountHome);
6401
6449
  }
@@ -6475,9 +6523,9 @@ class WegHeader {
6475
6523
  [options.className || '']: !!options.className,
6476
6524
  }, href: link.href, "aria-label": link.label, onClick: (event) => this.handleAuthClick(event, action, link.href, onNavigate) }, hAsync(Icon, null), !iconOnly ? link.label : null, iconOnly ? hAsync("span", { class: "sr-only" }, link.label) : null));
6477
6525
  }
6478
- renderManageAccountAnchor(onNavigate, iconOnly = false, ariaCurrentHref = null) {
6526
+ renderManageAccountAnchor(onNavigate, iconOnly = false, ariaCurrentHref = null, link) {
6479
6527
  const label = this.getManageAccountLabel();
6480
- const href = accountPath(this.getAccountHome(), '/account/manage');
6528
+ const href = this.resolveManageAccountHref(link);
6481
6529
  const active = this.isNavItemActive(href, 'prefix');
6482
6530
  const ariaCurrent = this.isAriaCurrentHref(href, ariaCurrentHref);
6483
6531
  return (hAsync("a", { class: {
@@ -6499,7 +6547,7 @@ class WegHeader {
6499
6547
  return (hAsync("li", { class: "main-nav__item main-nav__item--auth", key: "sign-in" }, this.renderAuthControl({ link: item, action: 'sign-in', onNavigate })));
6500
6548
  }
6501
6549
  if (isManageAccountLink(item, accountHome)) {
6502
- return (hAsync("li", { class: "main-nav__item main-nav__item--auth", key: "manage-account" }, this.renderManageAccountAnchor(onNavigate, false, ariaCurrentHref)));
6550
+ return (hAsync("li", { class: "main-nav__item main-nav__item--auth", key: "manage-account" }, this.renderManageAccountAnchor(onNavigate, false, ariaCurrentHref, item)));
6503
6551
  }
6504
6552
  if (this.signedIn && isFindJobLink(item)) {
6505
6553
  return this.renderIconNavLink(item, FindJobIcon, 'find-a-job-link', onNavigate, ariaCurrentHref);
@@ -6515,7 +6563,8 @@ class WegHeader {
6515
6563
  }
6516
6564
  renderCompactAuth(onNavigate) {
6517
6565
  if (this.signedIn) {
6518
- return this.renderManageAccountAnchor(onNavigate, true);
6566
+ const manageLink = this.findManageAccountLink(this.getActiveMenu());
6567
+ return this.renderManageAccountAnchor(onNavigate, true, null, manageLink ?? undefined);
6519
6568
  }
6520
6569
  const signIn = this.findSignInLink(this.resolved.menu);
6521
6570
  if (!signIn?.href)
@@ -6544,12 +6593,12 @@ class WegHeader {
6544
6593
  const logoSrc = this.getLogoSrc();
6545
6594
  const closeMenu = () => this.closeMenu();
6546
6595
  const mobileMenuActive = this.isMobileMenuActive();
6547
- return (hAsync(Host, { key: 'e0ed16abe9a49db723b627ea8bccfde470406f57', class: { 'weg-header--scroll-mode': this.headerScrollMode } }, hAsync("div", { key: '574712a024543f0653f9956949b2796377451847', class: "weg-header-shell" }, hAsync("a", { key: '279c196e293d93db62fc29915f63b7b24b38e6c1', class: "skip-to-content", href: "#", title: SKIP_TO_CONTENT_LABEL, onClick: (event) => this.handleSkipToContent(event) }, SKIP_TO_CONTENT_LABEL), hAsync("header", { key: '29bbed6bde5ccf8cd2b4e4df4a7ba58b122c2d6a', class: {
6596
+ return (hAsync(Host, { key: '916472d15df8abf5270650126356ef2d7a475765', class: { 'weg-header--scroll-mode': this.headerScrollMode } }, hAsync("div", { key: '8c8d63f67cf7b27364cc828aa42c5b011cbb971e', class: "weg-header-shell" }, hAsync("a", { key: '96f6141f0bff7a51bbc8f46ea9b92d0c33142e1a', class: "skip-to-content", href: "#", title: SKIP_TO_CONTENT_LABEL, onClick: (event) => this.handleSkipToContent(event) }, SKIP_TO_CONTENT_LABEL), hAsync("header", { key: '4a727a72e031159ca991505a1a8a59a32271aedf', class: {
6548
6597
  header: true,
6549
6598
  'header--menu-open': mobileMenuActive,
6550
6599
  'header--scroll-mode': this.headerScrollMode,
6551
6600
  'header--scroll-hidden': this.headerScrollHidden && this.headerScrollMode,
6552
- } }, hAsync("div", { key: '85650e9d0d244fefbd9f216130703de9288baa93', class: "header-inner" }, hAsync("div", { key: '34e7bf3d15733e9acefb12b2c77fee9a4cf7479c', class: "sr-only", "aria-live": "polite", "aria-atomic": "true", "data-weg-sr-live": true }), hAsync("button", { key: '9e35626afcecc92d93deb2fb25f8f93727f85d4d', type: "button", class: "menu-toggle icon-button", "aria-label": mobileMenuActive ? CLOSE_MAIN_NAV_LABEL : OPEN_MAIN_NAV_LABEL, "aria-expanded": mobileMenuActive ? 'true' : 'false', "aria-controls": "weg-header-main-nav", onClick: () => this.toggleMenu() }, mobileMenuActive ? hAsync(CloseIcon, null) : hAsync(HamburgerIcon, null)), hAsync(Logo, { key: '99ffa62e443e939316b58d36cab8e9e0a10cfa36', href: logoHref, src: logoSrc }), hAsync("div", { key: 'c9b453f5187d5b6ad782ae03400c13f16a31c383', class: "header-actions" }, this.renderCompactAuth(mobileMenuActive ? closeMenu : undefined)), hAsync("div", { key: '0f15f985cf4931a9867c341d053f5ebd2b398d42', class: "main-nav-panel", id: "weg-header-main-nav", role: mobileMenuActive ? 'dialog' : undefined, "aria-modal": mobileMenuActive ? 'true' : undefined, "aria-label": mobileMenuActive ? MAIN_NAV_LABEL : undefined }, this.renderNav(mobileMenuActive ? closeMenu : undefined)))))));
6601
+ } }, hAsync("div", { key: '5ef2a85bf2087206a17e3cbf163dde4d549dcd0b', class: "header-inner" }, hAsync("div", { key: '3af6990b5c6d93b4332a0d53300daddb11e2ad2b', class: "sr-only", "aria-live": "polite", "aria-atomic": "true", "data-weg-sr-live": true }), hAsync("button", { key: '023322f1e49a676a5b20208b956624ce820e9b57', type: "button", class: "menu-toggle icon-button", "aria-label": mobileMenuActive ? CLOSE_MAIN_NAV_LABEL : OPEN_MAIN_NAV_LABEL, "aria-expanded": mobileMenuActive ? 'true' : 'false', "aria-controls": "weg-header-main-nav", onClick: () => this.toggleMenu() }, mobileMenuActive ? hAsync(CloseIcon, null) : hAsync(HamburgerIcon, null)), hAsync(Logo, { key: 'efa51b76936456c49059c67e9f2c9c2365b97d25', href: logoHref, src: logoSrc }), hAsync("div", { key: 'ee0eff5ae3a63cf9550261a4d1727c9a88ab3e4d', class: "header-actions" }, this.renderCompactAuth(mobileMenuActive ? closeMenu : undefined)), hAsync("div", { key: '7552a6a661e644dcb3d555ab5572f680cd0295de', class: "main-nav-panel", id: "weg-header-main-nav", role: mobileMenuActive ? 'dialog' : undefined, "aria-modal": mobileMenuActive ? 'true' : undefined, "aria-label": mobileMenuActive ? MAIN_NAV_LABEL : undefined }, this.renderNav(mobileMenuActive ? closeMenu : undefined)))))));
6553
6602
  }
6554
6603
  static get watchers() { return {
6555
6604
  "layout": [{
package/hydrate/index.mjs CHANGED
@@ -5918,8 +5918,8 @@ class WegHeader {
5918
5918
  */
5919
5919
  userName;
5920
5920
  /**
5921
- * Account portal origin for sign-in, register, dashboard, and manage-account links.
5922
- * Defaults to production when omitted.
5921
+ * Account portal origin for the built-in signed-in fallback menu (when `header.menu` is empty).
5922
+ * Menu links from the CMS use their `href` as-is. Defaults to production when omitted.
5923
5923
  */
5924
5924
  accountBaseUrl;
5925
5925
  /**
@@ -6017,13 +6017,43 @@ class WegHeader {
6017
6017
  getMenuToggleButton() {
6018
6018
  return this.el.shadowRoot?.querySelector('.menu-toggle');
6019
6019
  }
6020
+ static FOCUSABLE_SELECTOR = 'a[href]:not([tabindex="-1"]), button:not([disabled]):not([tabindex="-1"])';
6020
6021
  getFirstNavFocusable() {
6021
6022
  const panel = this.el.shadowRoot?.querySelector('#weg-header-main-nav');
6022
6023
  if (!panel)
6023
6024
  return null;
6024
- const focusables = panel.querySelectorAll('a[href], button:not([disabled])');
6025
+ const focusables = panel.querySelectorAll(WegHeader.FOCUSABLE_SELECTOR);
6025
6026
  return focusables[0] ?? null;
6026
6027
  }
6028
+ /** Focusable controls inside the open mobile menu chrome (toggle → logo → auth → nav). */
6029
+ getMobileMenuFocusables() {
6030
+ const header = this.el.shadowRoot?.querySelector('.header');
6031
+ if (!header)
6032
+ return [];
6033
+ return Array.from(header.querySelectorAll(WegHeader.FOCUSABLE_SELECTOR));
6034
+ }
6035
+ trapMobileMenuFocus(event) {
6036
+ if (!this.isMobileMenuActive() || event.key !== 'Tab')
6037
+ return;
6038
+ const focusables = this.getMobileMenuFocusables();
6039
+ if (focusables.length === 0)
6040
+ return;
6041
+ const first = focusables[0];
6042
+ const last = focusables[focusables.length - 1];
6043
+ const active = this.el.shadowRoot?.activeElement;
6044
+ const activeInTrap = active != null && focusables.includes(active);
6045
+ if (event.shiftKey) {
6046
+ if (!activeInTrap || active === first) {
6047
+ event.preventDefault();
6048
+ last.focus();
6049
+ }
6050
+ return;
6051
+ }
6052
+ if (!activeInTrap || active === last) {
6053
+ event.preventDefault();
6054
+ first.focus();
6055
+ }
6056
+ }
6027
6057
  focusFirstNavItem() {
6028
6058
  setTimeout(() => {
6029
6059
  const attemptFocus = (retries = 8) => {
@@ -6135,6 +6165,10 @@ class WegHeader {
6135
6165
  this.resolve();
6136
6166
  }
6137
6167
  handleKeyDown(event) {
6168
+ if (event.key === 'Tab') {
6169
+ this.trapMobileMenuFocus(event);
6170
+ return;
6171
+ }
6138
6172
  if (event.key !== 'Escape')
6139
6173
  return;
6140
6174
  if (this.openDropdown) {
@@ -6394,6 +6428,20 @@ class WegHeader {
6394
6428
  const name = this.userName?.trim();
6395
6429
  return name || 'Manage Account';
6396
6430
  }
6431
+ resolveManageAccountHref(link) {
6432
+ const fromMenu = link?.href?.trim();
6433
+ if (fromMenu)
6434
+ return fromMenu;
6435
+ return accountPath(this.getAccountHome(), '/account/manage');
6436
+ }
6437
+ findManageAccountLink(menu) {
6438
+ const accountHome = this.getAccountHome();
6439
+ for (const item of menu) {
6440
+ if (!isMenuGroup(item) && isManageAccountLink(item, accountHome))
6441
+ return item;
6442
+ }
6443
+ return null;
6444
+ }
6397
6445
  shouldExcludeFromNavActive(item, accountHome) {
6398
6446
  return isSignOutLink(item) || isSignInLink(item, accountHome);
6399
6447
  }
@@ -6473,9 +6521,9 @@ class WegHeader {
6473
6521
  [options.className || '']: !!options.className,
6474
6522
  }, href: link.href, "aria-label": link.label, onClick: (event) => this.handleAuthClick(event, action, link.href, onNavigate) }, hAsync(Icon, null), !iconOnly ? link.label : null, iconOnly ? hAsync("span", { class: "sr-only" }, link.label) : null));
6475
6523
  }
6476
- renderManageAccountAnchor(onNavigate, iconOnly = false, ariaCurrentHref = null) {
6524
+ renderManageAccountAnchor(onNavigate, iconOnly = false, ariaCurrentHref = null, link) {
6477
6525
  const label = this.getManageAccountLabel();
6478
- const href = accountPath(this.getAccountHome(), '/account/manage');
6526
+ const href = this.resolveManageAccountHref(link);
6479
6527
  const active = this.isNavItemActive(href, 'prefix');
6480
6528
  const ariaCurrent = this.isAriaCurrentHref(href, ariaCurrentHref);
6481
6529
  return (hAsync("a", { class: {
@@ -6497,7 +6545,7 @@ class WegHeader {
6497
6545
  return (hAsync("li", { class: "main-nav__item main-nav__item--auth", key: "sign-in" }, this.renderAuthControl({ link: item, action: 'sign-in', onNavigate })));
6498
6546
  }
6499
6547
  if (isManageAccountLink(item, accountHome)) {
6500
- return (hAsync("li", { class: "main-nav__item main-nav__item--auth", key: "manage-account" }, this.renderManageAccountAnchor(onNavigate, false, ariaCurrentHref)));
6548
+ return (hAsync("li", { class: "main-nav__item main-nav__item--auth", key: "manage-account" }, this.renderManageAccountAnchor(onNavigate, false, ariaCurrentHref, item)));
6501
6549
  }
6502
6550
  if (this.signedIn && isFindJobLink(item)) {
6503
6551
  return this.renderIconNavLink(item, FindJobIcon, 'find-a-job-link', onNavigate, ariaCurrentHref);
@@ -6513,7 +6561,8 @@ class WegHeader {
6513
6561
  }
6514
6562
  renderCompactAuth(onNavigate) {
6515
6563
  if (this.signedIn) {
6516
- return this.renderManageAccountAnchor(onNavigate, true);
6564
+ const manageLink = this.findManageAccountLink(this.getActiveMenu());
6565
+ return this.renderManageAccountAnchor(onNavigate, true, null, manageLink ?? undefined);
6517
6566
  }
6518
6567
  const signIn = this.findSignInLink(this.resolved.menu);
6519
6568
  if (!signIn?.href)
@@ -6542,12 +6591,12 @@ class WegHeader {
6542
6591
  const logoSrc = this.getLogoSrc();
6543
6592
  const closeMenu = () => this.closeMenu();
6544
6593
  const mobileMenuActive = this.isMobileMenuActive();
6545
- return (hAsync(Host, { key: 'e0ed16abe9a49db723b627ea8bccfde470406f57', class: { 'weg-header--scroll-mode': this.headerScrollMode } }, hAsync("div", { key: '574712a024543f0653f9956949b2796377451847', class: "weg-header-shell" }, hAsync("a", { key: '279c196e293d93db62fc29915f63b7b24b38e6c1', class: "skip-to-content", href: "#", title: SKIP_TO_CONTENT_LABEL, onClick: (event) => this.handleSkipToContent(event) }, SKIP_TO_CONTENT_LABEL), hAsync("header", { key: '29bbed6bde5ccf8cd2b4e4df4a7ba58b122c2d6a', class: {
6594
+ return (hAsync(Host, { key: '916472d15df8abf5270650126356ef2d7a475765', class: { 'weg-header--scroll-mode': this.headerScrollMode } }, hAsync("div", { key: '8c8d63f67cf7b27364cc828aa42c5b011cbb971e', class: "weg-header-shell" }, hAsync("a", { key: '96f6141f0bff7a51bbc8f46ea9b92d0c33142e1a', class: "skip-to-content", href: "#", title: SKIP_TO_CONTENT_LABEL, onClick: (event) => this.handleSkipToContent(event) }, SKIP_TO_CONTENT_LABEL), hAsync("header", { key: '4a727a72e031159ca991505a1a8a59a32271aedf', class: {
6546
6595
  header: true,
6547
6596
  'header--menu-open': mobileMenuActive,
6548
6597
  'header--scroll-mode': this.headerScrollMode,
6549
6598
  'header--scroll-hidden': this.headerScrollHidden && this.headerScrollMode,
6550
- } }, hAsync("div", { key: '85650e9d0d244fefbd9f216130703de9288baa93', class: "header-inner" }, hAsync("div", { key: '34e7bf3d15733e9acefb12b2c77fee9a4cf7479c', class: "sr-only", "aria-live": "polite", "aria-atomic": "true", "data-weg-sr-live": true }), hAsync("button", { key: '9e35626afcecc92d93deb2fb25f8f93727f85d4d', type: "button", class: "menu-toggle icon-button", "aria-label": mobileMenuActive ? CLOSE_MAIN_NAV_LABEL : OPEN_MAIN_NAV_LABEL, "aria-expanded": mobileMenuActive ? 'true' : 'false', "aria-controls": "weg-header-main-nav", onClick: () => this.toggleMenu() }, mobileMenuActive ? hAsync(CloseIcon, null) : hAsync(HamburgerIcon, null)), hAsync(Logo, { key: '99ffa62e443e939316b58d36cab8e9e0a10cfa36', href: logoHref, src: logoSrc }), hAsync("div", { key: 'c9b453f5187d5b6ad782ae03400c13f16a31c383', class: "header-actions" }, this.renderCompactAuth(mobileMenuActive ? closeMenu : undefined)), hAsync("div", { key: '0f15f985cf4931a9867c341d053f5ebd2b398d42', class: "main-nav-panel", id: "weg-header-main-nav", role: mobileMenuActive ? 'dialog' : undefined, "aria-modal": mobileMenuActive ? 'true' : undefined, "aria-label": mobileMenuActive ? MAIN_NAV_LABEL : undefined }, this.renderNav(mobileMenuActive ? closeMenu : undefined)))))));
6599
+ } }, hAsync("div", { key: '5ef2a85bf2087206a17e3cbf163dde4d549dcd0b', class: "header-inner" }, hAsync("div", { key: '3af6990b5c6d93b4332a0d53300daddb11e2ad2b', class: "sr-only", "aria-live": "polite", "aria-atomic": "true", "data-weg-sr-live": true }), hAsync("button", { key: '023322f1e49a676a5b20208b956624ce820e9b57', type: "button", class: "menu-toggle icon-button", "aria-label": mobileMenuActive ? CLOSE_MAIN_NAV_LABEL : OPEN_MAIN_NAV_LABEL, "aria-expanded": mobileMenuActive ? 'true' : 'false', "aria-controls": "weg-header-main-nav", onClick: () => this.toggleMenu() }, mobileMenuActive ? hAsync(CloseIcon, null) : hAsync(HamburgerIcon, null)), hAsync(Logo, { key: 'efa51b76936456c49059c67e9f2c9c2365b97d25', href: logoHref, src: logoSrc }), hAsync("div", { key: 'ee0eff5ae3a63cf9550261a4d1727c9a88ab3e4d', class: "header-actions" }, this.renderCompactAuth(mobileMenuActive ? closeMenu : undefined)), hAsync("div", { key: '7552a6a661e644dcb3d555ab5572f680cd0295de', class: "main-nav-panel", id: "weg-header-main-nav", role: mobileMenuActive ? 'dialog' : undefined, "aria-modal": mobileMenuActive ? 'true' : undefined, "aria-label": mobileMenuActive ? MAIN_NAV_LABEL : undefined }, this.renderNav(mobileMenuActive ? closeMenu : undefined)))))));
6551
6600
  }
6552
6601
  static get watchers() { return {
6553
6602
  "layout": [{
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "weg-shared-layout",
3
- "version": "0.0.54",
3
+ "version": "0.0.58",
4
4
  "description": "Shared layout Web Components built with Stencil",
5
5
  "main": "dist/index.cjs.js",
6
6
  "module": "dist/index.js",
package/readme.md CHANGED
@@ -1,25 +1,34 @@
1
1
  # weg-shared-layout
2
2
 
3
- > Shared layout Web Components.
3
+ Shared layout Web Components built with Stencil.
4
4
 
5
5
  ## Install
6
6
 
7
7
  ```bash
8
8
  npm i weg-shared-layout
9
+ # or
10
+ pnpm add weg-shared-layout
9
11
  ```
10
12
 
11
- ## How it works
13
+ ## What this package contains
12
14
 
13
- `<weg-header>` and `<weg-footer>` are **presentational** Web Components: they do **not** fetch data.
15
+ `weg-shared-layout` publishes two presentational Web Components:
14
16
 
15
- Load layout from the **WEG CMS WEG21 API**, then pass the mapped object to `layout` on each tag:
17
+ - `<weg-header>`
18
+ - `<weg-footer>`
19
+
20
+ They do not fetch their own content. Your app loads layout data, maps it into the expected shape, and passes the same `layout` object to both components.
21
+
22
+ ## WEG21 API flow
23
+
24
+ The package includes helpers for the WEG CMS WEG21 API:
16
25
 
17
26
  | Endpoint | Purpose |
18
27
  | --- | --- |
19
- | `GET https://warwickemploymentgroup.com/api/v1/weg21` | Logo URLs, footer social + legal copy |
20
- | `GET …/api/v1/weg21/menus` | Header (`ext` / `int`) and footer link columns |
28
+ | `GET https://warwickemploymentgroup.com/api/v1/weg21` | Footer copy, social links, and additional bootstrap metadata such as logo/favicon URLs |
29
+ | `GET https://warwickemploymentgroup.com/api/v1/weg21/menus` | Header nav (`ext` / `int`) and footer link columns |
21
30
 
22
- Both require the `wcms-api-key` header. Use `fetchWeg21Layout` from `weg-shared-layout/menus`:
31
+ Use `fetchWeg21Layout()` to fetch and map both endpoints:
23
32
 
24
33
  ```ts
25
34
  import { fetchWeg21Layout } from 'weg-shared-layout/menus';
@@ -27,12 +36,16 @@ import { fetchWeg21Layout } from 'weg-shared-layout/menus';
27
36
  const layout = await fetchWeg21Layout({
28
37
  apiKey: process.env.WCMS_API_KEY,
29
38
  });
30
- // layout.header.menu, layout.footer.menu, layout.footer.social, …
31
39
  ```
32
40
 
33
- [`weg21-bootstrap.json`](src/assets/weg21-bootstrap.json) and [`weg21-menus.json`](src/assets/weg21-menus.json) mirror the **WEG21 API response schemas** (`GET /api/v1/weg21` and `/menus`). Map to `layout` with `dummyWeg21LayoutData()` or `menusToLayoutData`.
41
+ You can also work offline with the bundled schema fixtures:
42
+
43
+ - `weg-shared-layout/weg21-bootstrap.json`
44
+ - `weg-shared-layout/weg21-menus.json`
45
+
46
+ Map those with `dummyWeg21LayoutData()` or `menusToLayoutData()`.
34
47
 
35
- ### Layout shape (abbreviated)
48
+ ## Layout shape
36
49
 
37
50
  ```json
38
51
  {
@@ -51,33 +64,29 @@ const layout = await fetchWeg21Layout({
51
64
  }
52
65
  ```
53
66
 
54
- | Field | Used when | Notes |
55
- | --- | --- | --- |
56
- | `header.logoSrc` | Signed out | Optional CMS logo URL; bundled PNG if omitted |
57
- | `header.logoHref` | Always | Optional logo link URL; WEG homepage if omitted |
58
- | `header.menu` | Signed out | Groups use `items`; flat links use `href` |
59
- | `footer.menu` | Always | `LayoutLink[][]` — one column per inner array |
60
- | `footer.social` | Always | LinkedIn, Instagram, TikTok, or YouTube |
61
- | `footer.credits` / `footer.copyright` | Always | Legal copy |
62
-
63
- ### Auth (Sign in / Sign out)
67
+ | Field | Notes |
68
+ | --- | --- |
69
+ | `header.logoSrc` | Optional logo image URL; bundled logo is used when omitted |
70
+ | `header.logoHref` | Optional logo link URL; defaults to the WEG homepage |
71
+ | `header.menu` | Mixed array of groups (`items`) and flat links (`href`) |
72
+ | `footer.menu` | `LayoutLink[][]` where each inner array is a footer column |
73
+ | `footer.social` | LinkedIn, Instagram, TikTok, or YouTube links |
74
+ | `footer.credits` / `footer.copyright` | Footer legal copy |
64
75
 
65
- **Signed out**: renders `header.menu`. Sign-in is a normal link — add `{ "label": "Sign in", "href": "…" }` like Register or Career advice. The header recognizes it by label or login URL and follows `href` on click.
76
+ ## Header auth behavior
66
77
 
67
- **Signed in** (`signed-in` true): ignores CMS `header.menu` and shows built-in Find a job, Dashboard, Manage Account, and Sign out.
78
+ When `signed-in` is `false`, the header renders `layout.header.menu` directly. Sign-in should be represented as a normal CMS link such as `{ "label": "Sign in", "href": "…" }`.
68
79
 
69
- Set **`signed-in`** from your app session state. Pass **`user-name`** for Manage Account. Pass **`account-base-url`** when account links should point at a non-production portal. Pass **`current-path`** (e.g. `/career-advice/my-article`) to highlight the active nav item. **Sign out** emits **`wegAuthClick`** — the host must call `event.preventDefault()` and run your logout flow (API call, clear session, redirect).
80
+ When `signed-in` is `true`, the component still uses `layout.header.menu` if you provide one. If `layout.header.menu` is empty, it falls back to the built-in signed-in menu: Find a job, Dashboard, Manage Account, and Sign out.
70
81
 
71
- ### Nav active state
82
+ Pass:
72
83
 
73
- | Link type | Match rule |
74
- | --- | --- |
75
- | Flat nav link | Prefix — `/career-advice` matches `/career-advice/foo` |
76
- | Dropdown child | Exact pathname (+ query when href includes `?`) |
77
- | Dropdown parent | Active background when any child matches |
78
- | Sign in / Sign out | Never highlighted |
84
+ - `signed-in` from your app session state
85
+ - `user-name` for the signed-in Manage Account label
86
+ - `account-base-url` only when using the built-in signed-in fallback menu against a non-production account portal
87
+ - `current-path` to highlight the active nav item
79
88
 
80
- Active links use **`weg-purple-200`** (`#CDCFF8`). Omit `current-path` to disable active styling.
89
+ Sign-out emits `wegAuthClick`. The host app should call `event.preventDefault()` and run logout + redirect itself.
81
90
 
82
91
  ```js
83
92
  header.addEventListener('wegAuthClick', async (event) => {
@@ -88,34 +97,47 @@ header.addEventListener('wegAuthClick', async (event) => {
88
97
  });
89
98
  ```
90
99
 
91
- ### Package imports (TypeScript / bundlers)
100
+ ## Nav active state
101
+
102
+ | Link type | Match rule |
103
+ | --- | --- |
104
+ | Flat nav link | Prefix match, so `/career-advice` matches `/career-advice/foo` |
105
+ | Dropdown child | Exact pathname, plus query string when the href includes `?` |
106
+ | Dropdown parent | Active background when any child matches |
107
+ | Sign in / Sign out | Never highlighted |
108
+
109
+ Active links use `weg-purple-200` (`#CDCFF8`). Omit `current-path` to disable active styling.
110
+
111
+ ## Package entry points
92
112
 
93
- | Subpath | Use |
113
+ | Import | Purpose |
94
114
  | --- | --- |
95
- | `weg-shared-layout/loader` | `defineCustomElements()` — **recommended** |
96
- | `weg-shared-layout/menus` | `fetchWeg21Layout`, `menusToLayoutData` |
97
- | `weg-shared-layout/menus-data` | WEG21 API types (`MenusData`, …) |
98
- | `weg-shared-layout/layout-data` | `<weg-header>` / `<weg-footer>` types (`LayoutData`, …) |
99
- | `weg-shared-layout/weg21-bootstrap.json` | `GET /api/v1/weg21` schema sample |
100
- | `weg-shared-layout/weg21-menus.json` | `GET /api/v1/weg21/menus` schema sample |
101
- | `weg-shared-layout/weg-header` / `weg-footer` | Side-effect tag registration |
102
- | `weg-shared-layout/styles/container.css` | Shared `.container` width and gutter utility |
115
+ | `weg-shared-layout/loader` | `defineCustomElements()` for registering all tags |
116
+ | `weg-shared-layout/menus` | `fetchWeg21Layout`, `menusToLayoutData`, `dummyWeg21LayoutData` |
117
+ | `weg-shared-layout/menus-data` | WEG21 API response types |
118
+ | `weg-shared-layout/layout-data` | `layout` prop types |
119
+ | `weg-shared-layout/weg21-bootstrap.json` | Sample `GET /api/v1/weg21` payload |
120
+ | `weg-shared-layout/weg21-menus.json` | Sample `GET /api/v1/weg21/menus` payload |
121
+ | `weg-shared-layout/weg-header` / `weg-footer` | Side-effect imports for individual tags |
122
+ | `weg-shared-layout/hydrate` | Stencil hydrate build for server-side rendering workflows |
123
+ | `weg-shared-layout/styles/container.css` | Shared `.container` layout utility |
124
+ | `weg-shared-layout` | Root package export with generated JSX/component typings plus `format()` |
103
125
 
104
126
  ## Documentation
105
127
 
106
- Full index: **[docs/README.md](./docs/README.md)**.
128
+ See **[docs/README.md](./docs/README.md)** for the full index.
107
129
 
108
130
  | Guide | Doc |
109
131
  | --- | --- |
110
132
  | Angular 16 | [docs/angular.md](./docs/angular.md) |
111
- | Angular 16 demo (runnable) | [demo/angular16/README.md](./demo/angular16/README.md) |
112
- | React (SPA, client-only) | [docs/react.md](./docs/react.md) |
113
- | Next.js (App Router) | [docs/nextjs.md](./docs/nextjs.md) |
133
+ | Angular 16 demo | [demo/angular16/README.md](./demo/angular16/README.md) |
134
+ | React SPA | [docs/react.md](./docs/react.md) |
135
+ | Next.js App Router | [docs/nextjs.md](./docs/nextjs.md) |
114
136
  | Plain HTML / vanilla JS | [docs/vanilla.md](./docs/vanilla.md) |
115
- | Publishing (maintainers) | [docs/publishing.md](./docs/publishing.md) |
137
+ | Publishing | [docs/publishing.md](./docs/publishing.md) |
116
138
 
117
- ## Publishing (maintainers)
139
+ ## Publishing
118
140
 
119
- **`npm publish` does not run `npm run build` by itself** — it packs what is on disk. This package uses a **`prepack`** script so a full Stencil build runs before every `npm pack` / `npm publish`, avoiding incomplete tarballs.
141
+ `npm publish` does not run a build on its own; it publishes whatever is already on disk. This repo relies on `prepack` so a Stencil build runs before packing or publishing.
120
142
 
121
- Details, checklist, and CI notes: **[docs/publishing.md](./docs/publishing.md)**.
143
+ For the full release checklist, package contents, and CI notes, see **[docs/publishing.md](./docs/publishing.md)**.