@cronos-labs/ui 0.8.0 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -95,15 +95,16 @@ test: {
95
95
 
96
96
  ### Components
97
97
 
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
- | `Seo` | Renders nothing; imperatively upserts `<title>`, meta and canonical tags. Creates tags that are absent rather than skipping them |
103
- | `LocaleSelector` | Locale dropdown, shared by `Header` and `Footer` |
104
- | `SectionAnchorNav` | In-page anchor nav that tracks the active section |
105
- | `SearchField` | Labelled search input with optional clear button and status text |
106
- | `PillButton`, `FilterPillRow` | Filter pill primitives |
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 cut by tag. Publishing happens in CI so the tarball is always
220
- built on the same Node 22 runner consumers' pipelines use, rather than
221
- whatever happens to be on a laptop.
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 release workflow refuses to publish if the tag and `package.json`
233
- disagree on the version. `prepack` rebuilds `dist/` as part of packing, so
234
- a stale build can never ship.
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
 
@@ -1,38 +1,7 @@
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
- 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;
@@ -1,64 +1,10 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { Fragment, useEffect, useState } from 'react';
3
- import { LocaleSelector } from '../LocaleSelector/index.js';
4
- import { buildLocalizedPath } from '../locale/routes.js';
5
- import { useCurrentLocale } from '../locale/hooks.js';
6
- import { desktopViewportQuery } from '../tokens.js';
7
- import { useOptionalBrand } from '../brand/index.js';
8
- 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';
9
- const buildFooterHref = (target, locale, localize = true) => {
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
+ }
@@ -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" | "children" | "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"> & {
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" | "children" | "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;
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;
@@ -89,7 +89,12 @@ export const FooterDisclaimerStack = styled.div `
89
89
  }
90
90
  `;
91
91
  export const FooterDesktopLocaleSlot = styled.div `
92
- width: 197px;
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.5625rem;
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: flex;
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 !important;
269
- min-width: 0 !important;
270
- min-height: 0 !important;
271
- padding: 0 !important;
272
- border: 0 !important;
273
- background: transparent !important;
274
- color: ${({ theme }) => theme.colors.footerTextMuted} !important;
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 !important;
278
- font-weight: ${({ theme }) => theme.fontWeight.default} !important;
279
- line-height: 1.2727 !important;
280
- text-decoration: none !important;
281
- white-space: nowrap !important;
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
- background: transparent !important;
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 !important;
296
- line-height: 1.3333 !important;
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,2 @@
1
+ import type { FooterProps } from './types.js';
2
+ export declare function FooterView({ brandWordmarkSrc, columns, content, forceDocumentNavigation, legalLinks, onOpenCookiePreferences, waitlistSlot, }: FooterProps): React.JSX.Element;
@@ -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,4 +1,4 @@
1
- import type { HeaderPresentationVariant } from './index.js';
1
+ import type { HeaderPresentationVariant } from './types.js';
2
2
  export interface NavDropdownItem {
3
3
  key: string;
4
4
  label: string;
@@ -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 { useLocation } from 'react-router-dom';
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 location = useLocation();
9
+ const { pathname, hash } = useNavigationLocation();
10
10
  const menuId = buildMenuId(label);
11
11
  useEffect(() => {
12
12
  setIsOpen(false);
13
- }, [location.pathname, location.hash]);
13
+ }, [pathname, hash]);
14
14
  useEffect(() => {
15
15
  if (!isOpen)
16
16
  return;