@cronos-labs/ui 0.8.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +60 -16
- package/dist/Footer/index.d.ts +7 -38
- package/dist/Footer/index.js +9 -63
- package/dist/Footer/static.d.ts +16 -0
- package/dist/Footer/static.js +11 -0
- package/dist/Footer/styles.d.ts +2 -2
- package/dist/Footer/styles.js +29 -27
- package/dist/Footer/types.d.ts +41 -0
- package/dist/Footer/types.js +1 -0
- package/dist/Footer/view.d.ts +2 -0
- package/dist/Footer/view.js +55 -0
- package/dist/Header/NavDropdown.d.ts +1 -1
- package/dist/Header/NavDropdown.js +3 -3
- package/dist/Header/index.d.ts +7 -56
- package/dist/Header/index.js +9 -186
- package/dist/Header/static.d.ts +15 -0
- package/dist/Header/static.js +11 -0
- package/dist/Header/styles.d.ts +4 -1
- package/dist/Header/styles.js +27 -4
- package/dist/Header/types.d.ts +61 -0
- package/dist/Header/types.js +1 -0
- package/dist/Header/view.d.ts +2 -0
- package/dist/Header/view.js +184 -0
- package/dist/LocaleSelector/index.d.ts +8 -9
- package/dist/LocaleSelector/index.js +10 -61
- package/dist/LocaleSelector/view.d.ts +8 -0
- package/dist/LocaleSelector/view.js +59 -0
- package/dist/config/eslintConfig.d.ts +17 -0
- package/dist/config/eslintConfig.js +52 -23
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -0
- package/dist/navigation/context.d.ts +26 -0
- package/dist/navigation/context.js +18 -0
- package/dist/navigation/router.d.ts +11 -0
- package/dist/navigation/router.js +25 -0
- package/dist/navigation/static.d.ts +15 -0
- package/dist/navigation/static.js +19 -0
- package/dist/navigationContent.d.ts +6 -0
- package/dist/navigationContent.js +27 -0
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -95,15 +95,16 @@ test: {
|
|
|
95
95
|
|
|
96
96
|
### Components
|
|
97
97
|
|
|
98
|
-
| Export
|
|
99
|
-
|
|
|
100
|
-
| `Header`
|
|
101
|
-
| `Footer`
|
|
102
|
-
| `
|
|
103
|
-
| `
|
|
104
|
-
| `
|
|
105
|
-
| `
|
|
106
|
-
| `
|
|
98
|
+
| Export | Purpose |
|
|
99
|
+
| ------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
100
|
+
| `Header` | Site header: brand wordmark, nav with dropdowns, mobile drawer, locale selector, waitlist CTA |
|
|
101
|
+
| `Footer` | Site footer: link columns, legal links, cookie-preferences hook, disclaimer copy |
|
|
102
|
+
| `HeaderStatic` / `FooterStatic` | Router-free variants for statically rendered pages (Astro). Take `locale` and `pathname` as props, force document navigation, and never import `react-router-dom`, so the island needs no `<MemoryRouter>` |
|
|
103
|
+
| `Seo` | Renders nothing; imperatively upserts `<title>`, meta and canonical tags. Creates tags that are absent rather than skipping them |
|
|
104
|
+
| `LocaleSelector` | Locale dropdown, shared by `Header` and `Footer` |
|
|
105
|
+
| `SectionAnchorNav` | In-page anchor nav that tracks the active section |
|
|
106
|
+
| `SearchField` | Labelled search input with optional clear button and status text |
|
|
107
|
+
| `PillButton`, `FilterPillRow` | Filter pill primitives |
|
|
107
108
|
|
|
108
109
|
Header and Footer are driven entirely by props — no data fetching, no
|
|
109
110
|
router coupling beyond `react-router-dom`. Their content strings arrive
|
|
@@ -190,7 +191,27 @@ and React Refresh plugins, and disables rules Prettier owns.
|
|
|
190
191
|
|
|
191
192
|
## Local development
|
|
192
193
|
|
|
194
|
+
The toolchain is pinned, so every machine and CI resolve identically:
|
|
195
|
+
|
|
196
|
+
| Pinned by | File | Value |
|
|
197
|
+
| --------- | ---------------------------------- | -------------------- |
|
|
198
|
+
| Node | `.nvmrc` | 24.21.0 (active LTS) |
|
|
199
|
+
| npm | `packageManager` in `package.json` | 11.19.0 |
|
|
200
|
+
|
|
201
|
+
Use a version manager that reads `.nvmrc` (nvm, fnm, asdf) and enable Corepack
|
|
202
|
+
once — it ships with Node and resolves the npm named above.
|
|
203
|
+
|
|
204
|
+
This package deliberately publishes **no `engines` field**. The Node floor the
|
|
205
|
+
dev toolchain needs comes from a dev dependency, not from anything the library
|
|
206
|
+
does at runtime — it ships as bundled ESM and runs wherever its consumer runs.
|
|
207
|
+
Publishing that floor would impose a constraint that is not real, and every
|
|
208
|
+
consuming site sets `engine-strict=true`, so a wrong floor here would hard-fail
|
|
209
|
+
their `npm ci`. Pin the toolchain for contributors, impose nothing on
|
|
210
|
+
consumers.
|
|
211
|
+
|
|
193
212
|
```bash
|
|
213
|
+
nvm use # or: fnm use
|
|
214
|
+
corepack enable
|
|
194
215
|
npm ci
|
|
195
216
|
npm run lint
|
|
196
217
|
npm run typecheck
|
|
@@ -199,6 +220,12 @@ npm run build
|
|
|
199
220
|
npm run format:check
|
|
200
221
|
```
|
|
201
222
|
|
|
223
|
+
Use `npm ci`, not `npm install`, unless you are deliberately changing a
|
|
224
|
+
dependency: `packageManager` fixes the npm version because two npm majors
|
|
225
|
+
disagree about which optional peer entries belong in `package-lock.json`, and
|
|
226
|
+
a lockfile written by one and installed by the other fails `npm ci`. The
|
|
227
|
+
release workflow publishes with the same pinned npm.
|
|
228
|
+
|
|
202
229
|
`build` compiles `src/` to `dist/` with `tsc`, then rewrites every relative
|
|
203
230
|
specifier in `dist/**/*.js` and `dist/**/*.d.ts` to point at the concrete
|
|
204
231
|
emitted file. `tsc` emits extensionless specifiers under
|
|
@@ -216,9 +243,11 @@ Styled React units use `ComponentName/index.tsx` for the component and
|
|
|
216
243
|
|
|
217
244
|
## Releasing
|
|
218
245
|
|
|
219
|
-
Releases are
|
|
220
|
-
built on the
|
|
221
|
-
|
|
246
|
+
Releases are staged by tag and promoted by a human. The tarball is always
|
|
247
|
+
built on the pinned runner rather than whatever happens to be on a laptop,
|
|
248
|
+
and no credential in this repository can make a version live on its own: the
|
|
249
|
+
CI token is a granular **stage-only** token, so the most it can do is put a
|
|
250
|
+
build in front of a maintainer.
|
|
222
251
|
|
|
223
252
|
1. Land your change on `main` through a PR.
|
|
224
253
|
2. Bump `version` in `package.json` (semver).
|
|
@@ -229,11 +258,26 @@ whatever happens to be on a laptop.
|
|
|
229
258
|
git push origin ui-v<version>
|
|
230
259
|
```
|
|
231
260
|
|
|
232
|
-
The
|
|
233
|
-
|
|
234
|
-
|
|
261
|
+
The workflow refuses to continue if the tag and `package.json` disagree on
|
|
262
|
+
the version. `prepack` rebuilds `dist/` as part of packing, so a stale
|
|
263
|
+
build cannot ship.
|
|
264
|
+
|
|
265
|
+
4. CI runs `npm stage publish .`. The version is now staged and **not yet
|
|
266
|
+
installable** — `npm view @cronos-labs/ui version` still reports the
|
|
267
|
+
previous release.
|
|
268
|
+
|
|
269
|
+
5. A maintainer with 2FA promotes it. The run log prints the stage id:
|
|
270
|
+
|
|
271
|
+
```bash
|
|
272
|
+
npm stage list @cronos-labs/ui
|
|
273
|
+
npm stage view <stage-id> # inspect before shipping
|
|
274
|
+
npm stage download <stage-id> # or pull the tarball and check it
|
|
275
|
+
npm stage approve <stage-id> # publish
|
|
276
|
+
npm stage reject <stage-id> # discard
|
|
277
|
+
```
|
|
235
278
|
|
|
236
|
-
Consumers then bump their range and run `npm install`.
|
|
279
|
+
Consumers then bump their range and run `npm install`. A patch or minor
|
|
280
|
+
inside their existing caret range needs no bump — `npm install` picks it up.
|
|
237
281
|
|
|
238
282
|
## License
|
|
239
283
|
|
package/dist/Footer/index.d.ts
CHANGED
|
@@ -1,38 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
badge?: string;
|
|
9
|
-
}
|
|
10
|
-
export interface FooterLinkGroup {
|
|
11
|
-
group: string;
|
|
12
|
-
links: FooterLinkItem[];
|
|
13
|
-
}
|
|
14
|
-
export interface FooterLegalLinkItem {
|
|
15
|
-
label: string;
|
|
16
|
-
target: string;
|
|
17
|
-
external: boolean;
|
|
18
|
-
disabled?: boolean;
|
|
19
|
-
isCookiePreferences?: boolean;
|
|
20
|
-
}
|
|
21
|
-
export interface FooterContent {
|
|
22
|
-
localeSelectorAriaLabel: string;
|
|
23
|
-
legalNavLabel: string;
|
|
24
|
-
copyrightText: string;
|
|
25
|
-
disclaimer: string[];
|
|
26
|
-
}
|
|
27
|
-
interface FooterProps {
|
|
28
|
-
/** Overrides the wordmark from `<BrandProvider>`. Optional when one is present. */
|
|
29
|
-
brandWordmarkSrc?: string;
|
|
30
|
-
columns: FooterLinkGroup[];
|
|
31
|
-
content: FooterContent;
|
|
32
|
-
forceDocumentNavigation?: boolean;
|
|
33
|
-
legalLinks: FooterLegalLinkItem[];
|
|
34
|
-
onOpenCookiePreferences: () => void;
|
|
35
|
-
waitlistSlot?: ((legalMeta: React.ReactNode | undefined) => React.ReactNode) | undefined;
|
|
36
|
-
}
|
|
37
|
-
export declare function Footer({ brandWordmarkSrc, columns, content, forceDocumentNavigation, legalLinks, onOpenCookiePreferences, waitlistSlot, }: FooterProps): React.JSX.Element;
|
|
38
|
-
export {};
|
|
1
|
+
import type { FooterProps } from './types.js';
|
|
2
|
+
export type { FooterContent, FooterLegalLinkItem, FooterLinkGroup, FooterLinkItem, FooterProps, } from './types.js';
|
|
3
|
+
/**
|
|
4
|
+
* The site footer for apps with a react-router `<Router>` above it. Static
|
|
5
|
+
* sites without a router render `FooterStatic` instead.
|
|
6
|
+
*/
|
|
7
|
+
export declare function Footer(props: FooterProps): React.JSX.Element;
|
package/dist/Footer/index.js
CHANGED
|
@@ -1,64 +1,10 @@
|
|
|
1
|
-
import { jsx as _jsx
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
if (target.startsWith('/') && localize) {
|
|
11
|
-
return buildLocalizedPath(target, locale);
|
|
12
|
-
}
|
|
13
|
-
return target;
|
|
14
|
-
};
|
|
15
|
-
const getExternalLinkProps = (external) => {
|
|
16
|
-
if (!external) {
|
|
17
|
-
return {};
|
|
18
|
-
}
|
|
19
|
-
return { target: '_blank', rel: 'noreferrer' };
|
|
20
|
-
};
|
|
21
|
-
export function Footer({ brandWordmarkSrc, columns, content, forceDocumentNavigation = false, legalLinks, onOpenCookiePreferences, waitlistSlot, }) {
|
|
22
|
-
const brand = useOptionalBrand();
|
|
23
|
-
const wordmarkSrc = brandWordmarkSrc ?? brand?.wordmarkSrc;
|
|
24
|
-
if (wordmarkSrc === undefined) {
|
|
25
|
-
throw new Error('<Footer> needs a brandWordmarkSrc prop or a <BrandProvider>.');
|
|
26
|
-
}
|
|
27
|
-
const locale = useCurrentLocale();
|
|
28
|
-
const [isDesktopFooterLayout, setIsDesktopFooterLayout] = useState(true);
|
|
29
|
-
useEffect(() => {
|
|
30
|
-
const desktopMediaQuery = window.matchMedia(desktopViewportQuery);
|
|
31
|
-
const handleChange = (event) => {
|
|
32
|
-
setIsDesktopFooterLayout(event.matches);
|
|
33
|
-
};
|
|
34
|
-
setIsDesktopFooterLayout(desktopMediaQuery.matches);
|
|
35
|
-
desktopMediaQuery.addEventListener('change', handleChange);
|
|
36
|
-
return () => {
|
|
37
|
-
desktopMediaQuery.removeEventListener('change', handleChange);
|
|
38
|
-
};
|
|
39
|
-
}, []);
|
|
40
|
-
const renderFooterLink = (link) => {
|
|
41
|
-
if (link.disabled) {
|
|
42
|
-
return (_jsxs(FooterLinkDisabled, { "aria-disabled": "true", "aria-label": `${link.label} ${link.badge ?? ''}`.trim(), children: [_jsx("span", { children: link.label }), link.badge ? _jsx(FooterLinkBadge, { children: link.badge }) : null] }, link.target));
|
|
43
|
-
}
|
|
44
|
-
return (_jsx(FooterLinkStyled, { href: buildFooterHref(link.target, locale, link.localizeTarget ?? true), ...getExternalLinkProps(link.external), children: _jsx("span", { children: link.label }) }, link.target));
|
|
45
|
-
};
|
|
46
|
-
const renderLegalLink = (link) => {
|
|
47
|
-
if (link.isCookiePreferences) {
|
|
48
|
-
// Deliberately not OneTrust's `#ot-sdk-btn.ot-sdk-show-settings` hook:
|
|
49
|
-
// the SDK rewrites any element carrying it with the label configured in
|
|
50
|
-
// the OneTrust admin ("Cookies Settings"), replacing the localized copy
|
|
51
|
-
// and, on the Astro sites, changing the server-rendered text before the
|
|
52
|
-
// island hydrates — React then throws a hydration error and re-renders
|
|
53
|
-
// the whole tree on the client. `onOpenCookiePreferences` already opens
|
|
54
|
-
// the preference centre through the OneTrust API directly.
|
|
55
|
-
return (_jsx(FooterLegalButton, { type: "button", onClick: onOpenCookiePreferences, children: link.label }));
|
|
56
|
-
}
|
|
57
|
-
if (link.disabled) {
|
|
58
|
-
return _jsx(FooterLegalText, { "aria-disabled": "true", children: link.label });
|
|
59
|
-
}
|
|
60
|
-
return (_jsx(FooterLegalLinkStyled, { href: buildFooterHref(link.target, locale), ...getExternalLinkProps(link.external), children: link.label }));
|
|
61
|
-
};
|
|
62
|
-
const legalMeta = isDesktopFooterLayout ? (_jsxs(FooterLegalMeta, { children: [_jsx(FooterCopyright, { children: content.copyrightText }), _jsx(FooterLegalNav, { "aria-label": content.legalNavLabel, children: legalLinks.map((link, index) => (_jsxs("span", { children: [index > 0 ? _jsx(FooterLegalDivider, { "aria-hidden": "true" }) : null, renderLegalLink(link)] }, link.target))) })] })) : undefined;
|
|
63
|
-
return (_jsx(FooterShell, { children: _jsxs(FooterContainer, { children: [_jsxs(FooterGrid, { children: [_jsx(FooterWaitlistSlot, { children: waitlistSlot?.(legalMeta) }), _jsx(FooterMobileDivider, { "aria-hidden": "true" }), _jsxs(FooterColumnsGrid, { children: [columns.map((group) => (_jsxs(FooterColumn, { children: [_jsx(FooterColumnHeading, { children: group.group }), _jsx(FooterLinkList, { children: group.links.map((link) => renderFooterLink(link)) })] }, group.group))), _jsxs(FooterDisclaimerStack, { children: [isDesktopFooterLayout ? (_jsx(FooterDesktopLocaleSlot, { children: _jsx(LocaleSelector, { ariaLabel: content.localeSelectorAriaLabel, forceDocumentNavigation: forceDocumentNavigation }) })) : null, _jsx(FooterDisclaimer, { children: content.disclaimer.map((paragraph) => (_jsx("p", { children: paragraph }, paragraph))) })] })] }), !isDesktopFooterLayout ? (_jsxs(FooterMobileLegalMeta, { children: [_jsx(FooterLocaleSlot, { children: _jsx(LocaleSelector, { ariaLabel: content.localeSelectorAriaLabel, forceDocumentNavigation: forceDocumentNavigation }) }), _jsx(FooterMobileCopyright, { children: content.copyrightText }), _jsx(FooterMobileLegalNav, { "aria-label": content.legalNavLabel, children: legalLinks.map((link, index) => (_jsxs(Fragment, { children: [index > 0 ? (_jsx(FooterMobileLegalDividerTrack, { children: _jsx(FooterLegalDivider, { "aria-hidden": "true" }) })) : null, renderLegalLink(link)] }, link.target))) })] })) : null] }), _jsx(FooterWordmark, { "aria-hidden": "true", children: _jsx(FooterWordmarkImage, { src: wordmarkSrc, alt: "" }) })] }) }));
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { RouterNavigationProvider } from '../navigation/router.js';
|
|
3
|
+
import { FooterView } from './view.js';
|
|
4
|
+
/**
|
|
5
|
+
* The site footer for apps with a react-router `<Router>` above it. Static
|
|
6
|
+
* sites without a router render `FooterStatic` instead.
|
|
7
|
+
*/
|
|
8
|
+
export function Footer(props) {
|
|
9
|
+
return (_jsx(RouterNavigationProvider, { children: _jsx(FooterView, { ...props }) }));
|
|
64
10
|
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { AppLocale } from '../locale/types.js';
|
|
2
|
+
import type { FooterProps } from './types.js';
|
|
3
|
+
export interface FooterStaticProps extends Omit<FooterProps, 'forceDocumentNavigation'> {
|
|
4
|
+
/** The page's locale, resolved by the host (Astro) rather than a router param. */
|
|
5
|
+
locale: AppLocale;
|
|
6
|
+
/** The page's pathname, used to build the locale selector's targets. */
|
|
7
|
+
pathname: string;
|
|
8
|
+
search?: string | undefined;
|
|
9
|
+
hash?: string | undefined;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* The site footer for statically rendered pages with no client router. The
|
|
13
|
+
* locale selector performs a document navigation, so this variant never
|
|
14
|
+
* imports react-router and needs no `<MemoryRouter>` around the island.
|
|
15
|
+
*/
|
|
16
|
+
export declare function FooterStatic({ locale, pathname, search, hash, ...props }: FooterStaticProps): React.JSX.Element;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { StaticNavigationProvider } from '../navigation/static.js';
|
|
3
|
+
import { FooterView } from './view.js';
|
|
4
|
+
/**
|
|
5
|
+
* The site footer for statically rendered pages with no client router. The
|
|
6
|
+
* locale selector performs a document navigation, so this variant never
|
|
7
|
+
* imports react-router and needs no `<MemoryRouter>` around the island.
|
|
8
|
+
*/
|
|
9
|
+
export function FooterStatic({ locale, pathname, search, hash, ...props }) {
|
|
10
|
+
return (_jsx(StaticNavigationProvider, { locale: locale, pathname: pathname, search: search, hash: hash, children: _jsx(FooterView, { ...props, forceDocumentNavigation: true }) }));
|
|
11
|
+
}
|
package/dist/Footer/styles.d.ts
CHANGED
|
@@ -98,6 +98,6 @@ export declare const FooterWordmark: import("styled-components/dist/types").ISty
|
|
|
98
98
|
}> & string;
|
|
99
99
|
export declare const FooterWordmarkImage: import("styled-components/dist/types").IStyledComponentBase<"web", Omit<Omit<import("react").DetailedHTMLProps<import("react").ImgHTMLAttributes<HTMLImageElement>, HTMLImageElement>, "style"> & {
|
|
100
100
|
style?: import("react").CSSProperties | import("styled-components/dist/types").CSSPropertiesWithVars | undefined;
|
|
101
|
-
}, "property" | "title" | "slot" | "style" | "dir" | "content" | "rel" | "ref" | "key" | "defaultChecked" | "defaultValue" | "suppressContentEditableWarning" | "suppressHydrationWarning" | "accessKey" | "autoCapitalize" | "autoFocus" | "className" | "contentEditable" | "contextMenu" | "draggable" | "enterKeyHint" | "hidden" | "id" | "lang" | "nonce" | "spellCheck" | "tabIndex" | "translate" | "radioGroup" | "role" | "about" | "datatype" | "inlist" | "prefix" | "resource" | "rev" | "typeof" | "vocab" | "autoCorrect" | "autoSave" | "color" | "itemProp" | "itemScope" | "itemType" | "itemID" | "itemRef" | "results" | "security" | "unselectable" | "popover" | "popoverTargetAction" | "popoverTarget" | "inert" | "inputMode" | "is" | "exportparts" | "part" | "aria-activedescendant" | "aria-atomic" | "aria-autocomplete" | "aria-braillelabel" | "aria-brailleroledescription" | "aria-busy" | "aria-checked" | "aria-colcount" | "aria-colindex" | "aria-colindextext" | "aria-colspan" | "aria-controls" | "aria-current" | "aria-describedby" | "aria-description" | "aria-details" | "aria-disabled" | "aria-dropeffect" | "aria-errormessage" | "aria-expanded" | "aria-flowto" | "aria-grabbed" | "aria-haspopup" | "aria-hidden" | "aria-invalid" | "aria-keyshortcuts" | "aria-label" | "aria-labelledby" | "aria-level" | "aria-live" | "aria-modal" | "aria-multiline" | "aria-multiselectable" | "aria-orientation" | "aria-owns" | "aria-placeholder" | "aria-posinset" | "aria-pressed" | "aria-readonly" | "aria-relevant" | "aria-required" | "aria-roledescription" | "aria-rowcount" | "aria-rowindex" | "aria-rowindextext" | "aria-rowspan" | "aria-selected" | "aria-setsize" | "aria-sort" | "aria-valuemax" | "aria-valuemin" | "aria-valuenow" | "aria-valuetext" | "
|
|
101
|
+
}, "property" | "title" | "slot" | "style" | "dir" | "content" | "rel" | "children" | "ref" | "key" | "defaultChecked" | "defaultValue" | "suppressContentEditableWarning" | "suppressHydrationWarning" | "accessKey" | "autoCapitalize" | "autoFocus" | "className" | "contentEditable" | "contextMenu" | "draggable" | "enterKeyHint" | "hidden" | "id" | "lang" | "nonce" | "spellCheck" | "tabIndex" | "translate" | "radioGroup" | "role" | "about" | "datatype" | "inlist" | "prefix" | "resource" | "rev" | "typeof" | "vocab" | "autoCorrect" | "autoSave" | "color" | "itemProp" | "itemScope" | "itemType" | "itemID" | "itemRef" | "results" | "security" | "unselectable" | "popover" | "popoverTargetAction" | "popoverTarget" | "inert" | "inputMode" | "is" | "exportparts" | "part" | "aria-activedescendant" | "aria-atomic" | "aria-autocomplete" | "aria-braillelabel" | "aria-brailleroledescription" | "aria-busy" | "aria-checked" | "aria-colcount" | "aria-colindex" | "aria-colindextext" | "aria-colspan" | "aria-controls" | "aria-current" | "aria-describedby" | "aria-description" | "aria-details" | "aria-disabled" | "aria-dropeffect" | "aria-errormessage" | "aria-expanded" | "aria-flowto" | "aria-grabbed" | "aria-haspopup" | "aria-hidden" | "aria-invalid" | "aria-keyshortcuts" | "aria-label" | "aria-labelledby" | "aria-level" | "aria-live" | "aria-modal" | "aria-multiline" | "aria-multiselectable" | "aria-orientation" | "aria-owns" | "aria-placeholder" | "aria-posinset" | "aria-pressed" | "aria-readonly" | "aria-relevant" | "aria-required" | "aria-roledescription" | "aria-rowcount" | "aria-rowindex" | "aria-rowindextext" | "aria-rowspan" | "aria-selected" | "aria-setsize" | "aria-sort" | "aria-valuemax" | "aria-valuemin" | "aria-valuenow" | "aria-valuetext" | "dangerouslySetInnerHTML" | "onCopy" | "onCopyCapture" | "onCut" | "onCutCapture" | "onPaste" | "onPasteCapture" | "onCompositionEnd" | "onCompositionEndCapture" | "onCompositionStart" | "onCompositionStartCapture" | "onCompositionUpdate" | "onCompositionUpdateCapture" | "onFocus" | "onFocusCapture" | "onBlur" | "onBlurCapture" | "onChange" | "onChangeCapture" | "onBeforeInput" | "onBeforeInputCapture" | "onInput" | "onInputCapture" | "onReset" | "onResetCapture" | "onSubmit" | "onSubmitCapture" | "onInvalid" | "onInvalidCapture" | "onLoad" | "onLoadCapture" | "onError" | "onErrorCapture" | "onKeyDown" | "onKeyDownCapture" | "onKeyPress" | "onKeyPressCapture" | "onKeyUp" | "onKeyUpCapture" | "onAbort" | "onAbortCapture" | "onCanPlay" | "onCanPlayCapture" | "onCanPlayThrough" | "onCanPlayThroughCapture" | "onDurationChange" | "onDurationChangeCapture" | "onEmptied" | "onEmptiedCapture" | "onEncrypted" | "onEncryptedCapture" | "onEnded" | "onEndedCapture" | "onLoadedData" | "onLoadedDataCapture" | "onLoadedMetadata" | "onLoadedMetadataCapture" | "onLoadStart" | "onLoadStartCapture" | "onPause" | "onPauseCapture" | "onPlay" | "onPlayCapture" | "onPlaying" | "onPlayingCapture" | "onProgress" | "onProgressCapture" | "onRateChange" | "onRateChangeCapture" | "onSeeked" | "onSeekedCapture" | "onSeeking" | "onSeekingCapture" | "onStalled" | "onStalledCapture" | "onSuspend" | "onSuspendCapture" | "onTimeUpdate" | "onTimeUpdateCapture" | "onVolumeChange" | "onVolumeChangeCapture" | "onWaiting" | "onWaitingCapture" | "onAuxClick" | "onAuxClickCapture" | "onClick" | "onClickCapture" | "onContextMenu" | "onContextMenuCapture" | "onDoubleClick" | "onDoubleClickCapture" | "onDrag" | "onDragCapture" | "onDragEnd" | "onDragEndCapture" | "onDragEnter" | "onDragEnterCapture" | "onDragExit" | "onDragExitCapture" | "onDragLeave" | "onDragLeaveCapture" | "onDragOver" | "onDragOverCapture" | "onDragStart" | "onDragStartCapture" | "onDrop" | "onDropCapture" | "onMouseDown" | "onMouseDownCapture" | "onMouseEnter" | "onMouseLeave" | "onMouseMove" | "onMouseMoveCapture" | "onMouseOut" | "onMouseOutCapture" | "onMouseOver" | "onMouseOverCapture" | "onMouseUp" | "onMouseUpCapture" | "onSelect" | "onSelectCapture" | "onTouchCancel" | "onTouchCancelCapture" | "onTouchEnd" | "onTouchEndCapture" | "onTouchMove" | "onTouchMoveCapture" | "onTouchStart" | "onTouchStartCapture" | "onPointerDown" | "onPointerDownCapture" | "onPointerMove" | "onPointerMoveCapture" | "onPointerUp" | "onPointerUpCapture" | "onPointerCancel" | "onPointerCancelCapture" | "onPointerEnter" | "onPointerLeave" | "onPointerOver" | "onPointerOverCapture" | "onPointerOut" | "onPointerOutCapture" | "onGotPointerCapture" | "onGotPointerCaptureCapture" | "onLostPointerCapture" | "onLostPointerCaptureCapture" | "onScroll" | "onScrollCapture" | "onScrollEnd" | "onScrollEndCapture" | "onWheel" | "onWheelCapture" | "onAnimationStart" | "onAnimationStartCapture" | "onAnimationEnd" | "onAnimationEndCapture" | "onAnimationIteration" | "onAnimationIterationCapture" | "onToggle" | "onBeforeToggle" | "onTransitionCancel" | "onTransitionCancelCapture" | "onTransitionEnd" | "onTransitionEndCapture" | "onTransitionRun" | "onTransitionRunCapture" | "onTransitionStart" | "onTransitionStartCapture" | "referrerPolicy" | "alt" | "crossOrigin" | "decoding" | "fetchPriority" | "height" | "loading" | "sizes" | "src" | "srcSet" | "useMap" | "width"> & Partial<Pick<Omit<import("react").DetailedHTMLProps<import("react").ImgHTMLAttributes<HTMLImageElement>, HTMLImageElement>, "style"> & {
|
|
102
102
|
style?: import("react").CSSProperties | import("styled-components/dist/types").CSSPropertiesWithVars | undefined;
|
|
103
|
-
}, "property" | "title" | "slot" | "style" | "dir" | "content" | "rel" | "ref" | "key" | "defaultChecked" | "defaultValue" | "suppressContentEditableWarning" | "suppressHydrationWarning" | "accessKey" | "autoCapitalize" | "autoFocus" | "className" | "contentEditable" | "contextMenu" | "draggable" | "enterKeyHint" | "hidden" | "id" | "lang" | "nonce" | "spellCheck" | "tabIndex" | "translate" | "radioGroup" | "role" | "about" | "datatype" | "inlist" | "prefix" | "resource" | "rev" | "typeof" | "vocab" | "autoCorrect" | "autoSave" | "color" | "itemProp" | "itemScope" | "itemType" | "itemID" | "itemRef" | "results" | "security" | "unselectable" | "popover" | "popoverTargetAction" | "popoverTarget" | "inert" | "inputMode" | "is" | "exportparts" | "part" | "aria-activedescendant" | "aria-atomic" | "aria-autocomplete" | "aria-braillelabel" | "aria-brailleroledescription" | "aria-busy" | "aria-checked" | "aria-colcount" | "aria-colindex" | "aria-colindextext" | "aria-colspan" | "aria-controls" | "aria-current" | "aria-describedby" | "aria-description" | "aria-details" | "aria-disabled" | "aria-dropeffect" | "aria-errormessage" | "aria-expanded" | "aria-flowto" | "aria-grabbed" | "aria-haspopup" | "aria-hidden" | "aria-invalid" | "aria-keyshortcuts" | "aria-label" | "aria-labelledby" | "aria-level" | "aria-live" | "aria-modal" | "aria-multiline" | "aria-multiselectable" | "aria-orientation" | "aria-owns" | "aria-placeholder" | "aria-posinset" | "aria-pressed" | "aria-readonly" | "aria-relevant" | "aria-required" | "aria-roledescription" | "aria-rowcount" | "aria-rowindex" | "aria-rowindextext" | "aria-rowspan" | "aria-selected" | "aria-setsize" | "aria-sort" | "aria-valuemax" | "aria-valuemin" | "aria-valuenow" | "aria-valuetext" | "
|
|
103
|
+
}, "property" | "title" | "slot" | "style" | "dir" | "content" | "rel" | "children" | "ref" | "key" | "defaultChecked" | "defaultValue" | "suppressContentEditableWarning" | "suppressHydrationWarning" | "accessKey" | "autoCapitalize" | "autoFocus" | "className" | "contentEditable" | "contextMenu" | "draggable" | "enterKeyHint" | "hidden" | "id" | "lang" | "nonce" | "spellCheck" | "tabIndex" | "translate" | "radioGroup" | "role" | "about" | "datatype" | "inlist" | "prefix" | "resource" | "rev" | "typeof" | "vocab" | "autoCorrect" | "autoSave" | "color" | "itemProp" | "itemScope" | "itemType" | "itemID" | "itemRef" | "results" | "security" | "unselectable" | "popover" | "popoverTargetAction" | "popoverTarget" | "inert" | "inputMode" | "is" | "exportparts" | "part" | "aria-activedescendant" | "aria-atomic" | "aria-autocomplete" | "aria-braillelabel" | "aria-brailleroledescription" | "aria-busy" | "aria-checked" | "aria-colcount" | "aria-colindex" | "aria-colindextext" | "aria-colspan" | "aria-controls" | "aria-current" | "aria-describedby" | "aria-description" | "aria-details" | "aria-disabled" | "aria-dropeffect" | "aria-errormessage" | "aria-expanded" | "aria-flowto" | "aria-grabbed" | "aria-haspopup" | "aria-hidden" | "aria-invalid" | "aria-keyshortcuts" | "aria-label" | "aria-labelledby" | "aria-level" | "aria-live" | "aria-modal" | "aria-multiline" | "aria-multiselectable" | "aria-orientation" | "aria-owns" | "aria-placeholder" | "aria-posinset" | "aria-pressed" | "aria-readonly" | "aria-relevant" | "aria-required" | "aria-roledescription" | "aria-rowcount" | "aria-rowindex" | "aria-rowindextext" | "aria-rowspan" | "aria-selected" | "aria-setsize" | "aria-sort" | "aria-valuemax" | "aria-valuemin" | "aria-valuenow" | "aria-valuetext" | "dangerouslySetInnerHTML" | "onCopy" | "onCopyCapture" | "onCut" | "onCutCapture" | "onPaste" | "onPasteCapture" | "onCompositionEnd" | "onCompositionEndCapture" | "onCompositionStart" | "onCompositionStartCapture" | "onCompositionUpdate" | "onCompositionUpdateCapture" | "onFocus" | "onFocusCapture" | "onBlur" | "onBlurCapture" | "onChange" | "onChangeCapture" | "onBeforeInput" | "onBeforeInputCapture" | "onInput" | "onInputCapture" | "onReset" | "onResetCapture" | "onSubmit" | "onSubmitCapture" | "onInvalid" | "onInvalidCapture" | "onLoad" | "onLoadCapture" | "onError" | "onErrorCapture" | "onKeyDown" | "onKeyDownCapture" | "onKeyPress" | "onKeyPressCapture" | "onKeyUp" | "onKeyUpCapture" | "onAbort" | "onAbortCapture" | "onCanPlay" | "onCanPlayCapture" | "onCanPlayThrough" | "onCanPlayThroughCapture" | "onDurationChange" | "onDurationChangeCapture" | "onEmptied" | "onEmptiedCapture" | "onEncrypted" | "onEncryptedCapture" | "onEnded" | "onEndedCapture" | "onLoadedData" | "onLoadedDataCapture" | "onLoadedMetadata" | "onLoadedMetadataCapture" | "onLoadStart" | "onLoadStartCapture" | "onPause" | "onPauseCapture" | "onPlay" | "onPlayCapture" | "onPlaying" | "onPlayingCapture" | "onProgress" | "onProgressCapture" | "onRateChange" | "onRateChangeCapture" | "onSeeked" | "onSeekedCapture" | "onSeeking" | "onSeekingCapture" | "onStalled" | "onStalledCapture" | "onSuspend" | "onSuspendCapture" | "onTimeUpdate" | "onTimeUpdateCapture" | "onVolumeChange" | "onVolumeChangeCapture" | "onWaiting" | "onWaitingCapture" | "onAuxClick" | "onAuxClickCapture" | "onClick" | "onClickCapture" | "onContextMenu" | "onContextMenuCapture" | "onDoubleClick" | "onDoubleClickCapture" | "onDrag" | "onDragCapture" | "onDragEnd" | "onDragEndCapture" | "onDragEnter" | "onDragEnterCapture" | "onDragExit" | "onDragExitCapture" | "onDragLeave" | "onDragLeaveCapture" | "onDragOver" | "onDragOverCapture" | "onDragStart" | "onDragStartCapture" | "onDrop" | "onDropCapture" | "onMouseDown" | "onMouseDownCapture" | "onMouseEnter" | "onMouseLeave" | "onMouseMove" | "onMouseMoveCapture" | "onMouseOut" | "onMouseOutCapture" | "onMouseOver" | "onMouseOverCapture" | "onMouseUp" | "onMouseUpCapture" | "onSelect" | "onSelectCapture" | "onTouchCancel" | "onTouchCancelCapture" | "onTouchEnd" | "onTouchEndCapture" | "onTouchMove" | "onTouchMoveCapture" | "onTouchStart" | "onTouchStartCapture" | "onPointerDown" | "onPointerDownCapture" | "onPointerMove" | "onPointerMoveCapture" | "onPointerUp" | "onPointerUpCapture" | "onPointerCancel" | "onPointerCancelCapture" | "onPointerEnter" | "onPointerLeave" | "onPointerOver" | "onPointerOverCapture" | "onPointerOut" | "onPointerOutCapture" | "onGotPointerCapture" | "onGotPointerCaptureCapture" | "onLostPointerCapture" | "onLostPointerCaptureCapture" | "onScroll" | "onScrollCapture" | "onScrollEnd" | "onScrollEndCapture" | "onWheel" | "onWheelCapture" | "onAnimationStart" | "onAnimationStartCapture" | "onAnimationEnd" | "onAnimationEndCapture" | "onAnimationIteration" | "onAnimationIterationCapture" | "onToggle" | "onBeforeToggle" | "onTransitionCancel" | "onTransitionCancelCapture" | "onTransitionEnd" | "onTransitionEndCapture" | "onTransitionRun" | "onTransitionRunCapture" | "onTransitionStart" | "onTransitionStartCapture" | "referrerPolicy" | "alt" | "crossOrigin" | "decoding" | "fetchPriority" | "height" | "loading" | "sizes" | "src" | "srcSet" | "useMap" | "width">>> & string;
|
package/dist/Footer/styles.js
CHANGED
|
@@ -89,7 +89,12 @@ export const FooterDisclaimerStack = styled.div `
|
|
|
89
89
|
}
|
|
90
90
|
`;
|
|
91
91
|
export const FooterDesktopLocaleSlot = styled.div `
|
|
92
|
-
|
|
92
|
+
display: none;
|
|
93
|
+
|
|
94
|
+
${({ theme }) => theme.mediaQueries.lg} {
|
|
95
|
+
display: block;
|
|
96
|
+
width: 197px;
|
|
97
|
+
}
|
|
93
98
|
`;
|
|
94
99
|
export const FooterDisclaimer = styled.div `
|
|
95
100
|
display: grid;
|
|
@@ -198,20 +203,23 @@ export const FooterLinkBadge = styled.span `
|
|
|
198
203
|
border-radius: ${({ theme }) => theme.borderRadius.pill};
|
|
199
204
|
background: rgb(0 0 0 / 0.1);
|
|
200
205
|
color: currentColor;
|
|
201
|
-
font-size: 0.
|
|
206
|
+
font-size: 0.6875rem;
|
|
202
207
|
font-weight: ${({ theme }) => theme.fontWeight.black};
|
|
203
208
|
letter-spacing: 0.1em;
|
|
204
209
|
line-height: 1;
|
|
205
210
|
text-transform: uppercase;
|
|
206
211
|
white-space: nowrap;
|
|
207
212
|
`;
|
|
213
|
+
// Desktop-only: below the breakpoint FooterMobileLegalMeta carries the same
|
|
214
|
+
// copyright and legal links, so this block is hidden by CSS rather than left
|
|
215
|
+
// out of the tree (see Footer/view.tsx).
|
|
208
216
|
export const FooterLegalMeta = styled.div `
|
|
209
|
-
display:
|
|
210
|
-
flex-wrap: wrap;
|
|
211
|
-
align-items: center;
|
|
212
|
-
gap: 12px;
|
|
217
|
+
display: none;
|
|
213
218
|
|
|
214
219
|
${({ theme }) => theme.mediaQueries.lg} {
|
|
220
|
+
display: flex;
|
|
221
|
+
flex-wrap: wrap;
|
|
222
|
+
align-items: center;
|
|
215
223
|
gap: 10px 12px;
|
|
216
224
|
}
|
|
217
225
|
`;
|
|
@@ -260,30 +268,24 @@ export const FooterLegalLink = styled.a `
|
|
|
260
268
|
line-height: 1.3333;
|
|
261
269
|
}
|
|
262
270
|
`;
|
|
263
|
-
// The `!important`s below date from when this button carried OneTrust's
|
|
264
|
-
// `#ot-sdk-btn.ot-sdk-show-settings` hook and had to beat the SDK's own
|
|
265
|
-
// button styles. The hook is gone (see Footer/index.tsx); they are kept for
|
|
266
|
-
// now so this change stays limited to the label/hydration fix.
|
|
267
271
|
export const FooterLegalButton = styled.button `
|
|
268
|
-
display: inline
|
|
269
|
-
min-width: 0
|
|
270
|
-
min-height: 0
|
|
271
|
-
padding: 0
|
|
272
|
-
border: 0
|
|
273
|
-
background: transparent
|
|
274
|
-
color: ${({ theme }) => theme.colors.footerTextMuted}
|
|
272
|
+
display: inline;
|
|
273
|
+
min-width: 0;
|
|
274
|
+
min-height: 0;
|
|
275
|
+
padding: 0;
|
|
276
|
+
border: 0;
|
|
277
|
+
background: transparent;
|
|
278
|
+
color: ${({ theme }) => theme.colors.footerTextMuted};
|
|
275
279
|
cursor: pointer;
|
|
276
280
|
font: inherit;
|
|
277
|
-
font-size: 0.6875rem
|
|
278
|
-
font-weight: ${({ theme }) => theme.fontWeight.default}
|
|
279
|
-
line-height: 1.2727
|
|
280
|
-
text-decoration: none
|
|
281
|
-
white-space: nowrap
|
|
281
|
+
font-size: 0.6875rem;
|
|
282
|
+
font-weight: ${({ theme }) => theme.fontWeight.default};
|
|
283
|
+
line-height: 1.2727;
|
|
284
|
+
text-decoration: none;
|
|
285
|
+
white-space: nowrap;
|
|
282
286
|
|
|
283
287
|
&:hover {
|
|
284
|
-
|
|
285
|
-
color: ${({ theme }) => theme.colors.footerTextMuted} !important;
|
|
286
|
-
text-decoration: underline !important;
|
|
288
|
+
text-decoration: underline;
|
|
287
289
|
}
|
|
288
290
|
|
|
289
291
|
&:focus-visible {
|
|
@@ -292,8 +294,8 @@ export const FooterLegalButton = styled.button `
|
|
|
292
294
|
}
|
|
293
295
|
|
|
294
296
|
${({ theme }) => theme.mediaQueries.lg} {
|
|
295
|
-
font-size: 0.75rem
|
|
296
|
-
line-height: 1.3333
|
|
297
|
+
font-size: 0.75rem;
|
|
298
|
+
line-height: 1.3333;
|
|
297
299
|
}
|
|
298
300
|
`;
|
|
299
301
|
export const FooterLegalText = styled.span `
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export interface FooterLinkItem {
|
|
2
|
+
label: string;
|
|
3
|
+
target: string;
|
|
4
|
+
external: boolean;
|
|
5
|
+
localizeTarget?: boolean;
|
|
6
|
+
/** Renders the link greyed and non-interactive, with `badge` beside it if given. */
|
|
7
|
+
disabled?: boolean;
|
|
8
|
+
badge?: string;
|
|
9
|
+
}
|
|
10
|
+
export interface FooterLinkGroup {
|
|
11
|
+
group: string;
|
|
12
|
+
links: FooterLinkItem[];
|
|
13
|
+
}
|
|
14
|
+
export interface FooterLegalLinkItem {
|
|
15
|
+
label: string;
|
|
16
|
+
target: string;
|
|
17
|
+
external: boolean;
|
|
18
|
+
disabled?: boolean;
|
|
19
|
+
isCookiePreferences?: boolean;
|
|
20
|
+
}
|
|
21
|
+
export interface FooterContent {
|
|
22
|
+
localeSelectorAriaLabel: string;
|
|
23
|
+
legalNavLabel: string;
|
|
24
|
+
copyrightText: string;
|
|
25
|
+
disclaimer: string[];
|
|
26
|
+
}
|
|
27
|
+
export interface FooterProps {
|
|
28
|
+
/** Overrides the wordmark from `<BrandProvider>`. Optional when one is present. */
|
|
29
|
+
brandWordmarkSrc?: string;
|
|
30
|
+
columns: FooterLinkGroup[];
|
|
31
|
+
content: FooterContent;
|
|
32
|
+
forceDocumentNavigation?: boolean;
|
|
33
|
+
legalLinks: FooterLegalLinkItem[];
|
|
34
|
+
onOpenCookiePreferences: () => void;
|
|
35
|
+
/**
|
|
36
|
+
* Renders the footer's CTA column. Receives the copyright + legal-links
|
|
37
|
+
* block, which is hidden below the desktop breakpoint by CSS, so render it
|
|
38
|
+
* unconditionally and let the footer's own mobile legal block take over.
|
|
39
|
+
*/
|
|
40
|
+
waitlistSlot?: ((legalMeta: React.ReactNode) => React.ReactNode) | undefined;
|
|
41
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Fragment } from 'react';
|
|
3
|
+
import { LocaleSelectorView } from '../LocaleSelector/view.js';
|
|
4
|
+
import { buildLocalizedPath } from '../locale/routes.js';
|
|
5
|
+
import { useNavigationLocation } from '../navigation/context.js';
|
|
6
|
+
import { useOptionalBrand } from '../brand/index.js';
|
|
7
|
+
import { FooterColumn, FooterColumnHeading, FooterContainer, FooterCopyright, FooterColumnsGrid, FooterDesktopLocaleSlot, FooterDisclaimer, FooterDisclaimerStack, FooterGrid, FooterLegalButton, FooterLegalDivider, FooterLegalLink as FooterLegalLinkStyled, FooterLegalMeta, FooterLegalNav, FooterLegalText, FooterLocaleSlot, FooterLink as FooterLinkStyled, FooterLinkBadge, FooterLinkDisabled, FooterLinkList, FooterMobileCopyright, FooterMobileDivider, FooterMobileLegalDividerTrack, FooterMobileLegalMeta, FooterMobileLegalNav, FooterShell, FooterWaitlistSlot, FooterWordmark, FooterWordmarkImage, } from './styles.js';
|
|
8
|
+
const buildFooterHref = (target, locale, localize = true) => {
|
|
9
|
+
if (target.startsWith('/') && localize) {
|
|
10
|
+
return buildLocalizedPath(target, locale);
|
|
11
|
+
}
|
|
12
|
+
return target;
|
|
13
|
+
};
|
|
14
|
+
const getExternalLinkProps = (external) => {
|
|
15
|
+
if (!external) {
|
|
16
|
+
return {};
|
|
17
|
+
}
|
|
18
|
+
return { target: '_blank', rel: 'noreferrer' };
|
|
19
|
+
};
|
|
20
|
+
export function FooterView({ brandWordmarkSrc, columns, content, forceDocumentNavigation = false, legalLinks, onOpenCookiePreferences, waitlistSlot, }) {
|
|
21
|
+
const brand = useOptionalBrand();
|
|
22
|
+
const wordmarkSrc = brandWordmarkSrc ?? brand?.wordmarkSrc;
|
|
23
|
+
if (wordmarkSrc === undefined) {
|
|
24
|
+
throw new Error('<Footer> needs a brandWordmarkSrc prop or a <BrandProvider>.');
|
|
25
|
+
}
|
|
26
|
+
const { locale } = useNavigationLocation();
|
|
27
|
+
const renderFooterLink = (link) => {
|
|
28
|
+
if (link.disabled) {
|
|
29
|
+
return (_jsxs(FooterLinkDisabled, { "aria-disabled": "true", "aria-label": `${link.label} ${link.badge ?? ''}`.trim(), children: [_jsx("span", { children: link.label }), link.badge ? _jsx(FooterLinkBadge, { children: link.badge }) : null] }, link.target));
|
|
30
|
+
}
|
|
31
|
+
return (_jsx(FooterLinkStyled, { href: buildFooterHref(link.target, locale, link.localizeTarget ?? true), ...getExternalLinkProps(link.external), children: _jsx("span", { children: link.label }) }, link.target));
|
|
32
|
+
};
|
|
33
|
+
const renderLegalLink = (link) => {
|
|
34
|
+
if (link.isCookiePreferences) {
|
|
35
|
+
// Deliberately not OneTrust's `#ot-sdk-btn.ot-sdk-show-settings` hook:
|
|
36
|
+
// the SDK rewrites any element carrying it with the label configured in
|
|
37
|
+
// the OneTrust admin ("Cookies Settings"), replacing the localized copy
|
|
38
|
+
// and, on the Astro sites, changing the server-rendered text before the
|
|
39
|
+
// island hydrates — React then throws a hydration error and re-renders
|
|
40
|
+
// the whole tree on the client. `onOpenCookiePreferences` already opens
|
|
41
|
+
// the preference centre through the OneTrust API directly.
|
|
42
|
+
return (_jsx(FooterLegalButton, { type: "button", onClick: onOpenCookiePreferences, children: link.label }));
|
|
43
|
+
}
|
|
44
|
+
if (link.disabled) {
|
|
45
|
+
return _jsx(FooterLegalText, { "aria-disabled": "true", children: link.label });
|
|
46
|
+
}
|
|
47
|
+
return (_jsx(FooterLegalLinkStyled, { href: buildFooterHref(link.target, locale), ...getExternalLinkProps(link.external), children: link.label }));
|
|
48
|
+
};
|
|
49
|
+
// Both the desktop legal block (handed to waitlistSlot) and the mobile one
|
|
50
|
+
// are always rendered and swapped by CSS media queries, so the server and
|
|
51
|
+
// the first client render agree at every viewport and nothing re-lays out
|
|
52
|
+
// after hydration.
|
|
53
|
+
const legalMeta = (_jsxs(FooterLegalMeta, { children: [_jsx(FooterCopyright, { children: content.copyrightText }), _jsx(FooterLegalNav, { "aria-label": content.legalNavLabel, children: legalLinks.map((link, index) => (_jsxs("span", { children: [index > 0 ? _jsx(FooterLegalDivider, { "aria-hidden": "true" }) : null, renderLegalLink(link)] }, link.target))) })] }));
|
|
54
|
+
return (_jsx(FooterShell, { children: _jsxs(FooterContainer, { children: [_jsxs(FooterGrid, { children: [_jsx(FooterWaitlistSlot, { children: waitlistSlot?.(legalMeta) }), _jsx(FooterMobileDivider, { "aria-hidden": "true" }), _jsxs(FooterColumnsGrid, { children: [columns.map((group) => (_jsxs(FooterColumn, { children: [_jsx(FooterColumnHeading, { children: group.group }), _jsx(FooterLinkList, { children: group.links.map((link) => renderFooterLink(link)) })] }, group.group))), _jsxs(FooterDisclaimerStack, { children: [_jsx(FooterDesktopLocaleSlot, { children: _jsx(LocaleSelectorView, { ariaLabel: content.localeSelectorAriaLabel, forceDocumentNavigation: forceDocumentNavigation }) }), _jsx(FooterDisclaimer, { children: content.disclaimer.map((paragraph) => (_jsx("p", { children: paragraph }, paragraph))) })] })] }), _jsxs(FooterMobileLegalMeta, { children: [_jsx(FooterLocaleSlot, { children: _jsx(LocaleSelectorView, { ariaLabel: content.localeSelectorAriaLabel, forceDocumentNavigation: forceDocumentNavigation }) }), _jsx(FooterMobileCopyright, { children: content.copyrightText }), _jsx(FooterMobileLegalNav, { "aria-label": content.legalNavLabel, children: legalLinks.map((link, index) => (_jsxs(Fragment, { children: [index > 0 ? (_jsx(FooterMobileLegalDividerTrack, { children: _jsx(FooterLegalDivider, { "aria-hidden": "true" }) })) : null, renderLegalLink(link)] }, link.target))) })] })] }), _jsx(FooterWordmark, { "aria-hidden": "true", children: _jsx(FooterWordmarkImage, { src: wordmarkSrc, alt: "" }) })] }) }));
|
|
55
|
+
}
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useEffect, useRef, useState } from 'react';
|
|
3
|
-
import {
|
|
3
|
+
import { useNavigationLocation } from '../navigation/context.js';
|
|
4
4
|
import { NavDropdown as NavDropdownRoot, NavDropdownChevron, NavDropdownDisabledOption, NavDropdownLabel, NavDropdownList, NavDropdownMenu, NavDropdownOption, NavDropdownOptionBadge, NavDropdownTrigger, } from './styles.js';
|
|
5
5
|
const buildMenuId = (label) => `header-nav-dropdown-${label.toLowerCase().replace(/\s+/gu, '-')}`;
|
|
6
6
|
export function NavDropdown({ label, items, variant, contrast, }) {
|
|
7
7
|
const [isOpen, setIsOpen] = useState(false);
|
|
8
8
|
const rootRef = useRef(null);
|
|
9
|
-
const
|
|
9
|
+
const { pathname, hash } = useNavigationLocation();
|
|
10
10
|
const menuId = buildMenuId(label);
|
|
11
11
|
useEffect(() => {
|
|
12
12
|
setIsOpen(false);
|
|
13
|
-
}, [
|
|
13
|
+
}, [pathname, hash]);
|
|
14
14
|
useEffect(() => {
|
|
15
15
|
if (!isOpen)
|
|
16
16
|
return;
|