@cronos-labs/ui 0.7.2 → 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,35 +1,7 @@
1
- export interface FooterLinkItem {
2
- label: string;
3
- target: string;
4
- external: boolean;
5
- localizeTarget?: boolean;
6
- }
7
- export interface FooterLinkGroup {
8
- group: string;
9
- links: FooterLinkItem[];
10
- }
11
- export interface FooterLegalLinkItem {
12
- label: string;
13
- target: string;
14
- external: boolean;
15
- disabled?: boolean;
16
- isCookiePreferences?: boolean;
17
- }
18
- export interface FooterContent {
19
- localeSelectorAriaLabel: string;
20
- legalNavLabel: string;
21
- copyrightText: string;
22
- disclaimer: string[];
23
- }
24
- interface FooterProps {
25
- /** Overrides the wordmark from `<BrandProvider>`. Optional when one is present. */
26
- brandWordmarkSrc?: string;
27
- columns: FooterLinkGroup[];
28
- content: FooterContent;
29
- forceDocumentNavigation?: boolean;
30
- legalLinks: FooterLegalLinkItem[];
31
- onOpenCookiePreferences: () => void;
32
- waitlistSlot?: ((legalMeta: React.ReactNode | undefined) => React.ReactNode) | undefined;
33
- }
34
- export declare function Footer({ brandWordmarkSrc, columns, content, forceDocumentNavigation, legalLinks, onOpenCookiePreferences, waitlistSlot, }: FooterProps): React.JSX.Element;
35
- 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,61 +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, 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
- return (_jsx(FooterLinkStyled, { href: buildFooterHref(link.target, locale, link.localizeTarget ?? true), ...getExternalLinkProps(link.external), children: _jsx("span", { children: link.label }) }, link.target));
42
- };
43
- const renderLegalLink = (link) => {
44
- if (link.isCookiePreferences) {
45
- // Deliberately not OneTrust's `#ot-sdk-btn.ot-sdk-show-settings` hook:
46
- // the SDK rewrites any element carrying it with the label configured in
47
- // the OneTrust admin ("Cookies Settings"), replacing the localized copy
48
- // and, on the Astro sites, changing the server-rendered text before the
49
- // island hydrates — React then throws a hydration error and re-renders
50
- // the whole tree on the client. `onOpenCookiePreferences` already opens
51
- // the preference centre through the OneTrust API directly.
52
- return (_jsx(FooterLegalButton, { type: "button", onClick: onOpenCookiePreferences, children: link.label }));
53
- }
54
- if (link.disabled) {
55
- return _jsx(FooterLegalText, { "aria-disabled": "true", children: link.label });
56
- }
57
- return (_jsx(FooterLegalLinkStyled, { href: buildFooterHref(link.target, locale), ...getExternalLinkProps(link.external), children: link.label }));
58
- };
59
- 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;
60
- 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 }) }));
61
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
+ }
@@ -40,6 +40,12 @@ export declare const FooterLinkList: import("styled-components/dist/types").ISty
40
40
  export declare const FooterLink: import("styled-components/dist/types").IStyledComponentBase<"web", Omit<import("react").DetailedHTMLProps<import("react").AnchorHTMLAttributes<HTMLAnchorElement>, HTMLAnchorElement>, "style"> & {
41
41
  style?: import("react").CSSProperties | import("styled-components/dist/types").CSSPropertiesWithVars | undefined;
42
42
  }> & string;
43
+ export declare const FooterLinkDisabled: import("styled-components/dist/types").IStyledComponentBase<"web", Omit<import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLSpanElement>, HTMLSpanElement>, "style"> & {
44
+ style?: import("react").CSSProperties | import("styled-components/dist/types").CSSPropertiesWithVars | undefined;
45
+ }> & string;
46
+ export declare const FooterLinkBadge: import("styled-components/dist/types").IStyledComponentBase<"web", Omit<import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLSpanElement>, HTMLSpanElement>, "style"> & {
47
+ style?: import("react").CSSProperties | import("styled-components/dist/types").CSSPropertiesWithVars | undefined;
48
+ }> & string;
43
49
  export declare const FooterLegalMeta: import("styled-components/dist/types").IStyledComponentBase<"web", Omit<import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLDivElement>, HTMLDivElement>, "style"> & {
44
50
  style?: import("react").CSSProperties | import("styled-components/dist/types").CSSPropertiesWithVars | undefined;
45
51
  }> & string;
@@ -92,6 +98,6 @@ export declare const FooterWordmark: import("styled-components/dist/types").ISty
92
98
  }> & string;
93
99
  export declare const FooterWordmarkImage: import("styled-components/dist/types").IStyledComponentBase<"web", Omit<Omit<import("react").DetailedHTMLProps<import("react").ImgHTMLAttributes<HTMLImageElement>, HTMLImageElement>, "style"> & {
94
100
  style?: import("react").CSSProperties | import("styled-components/dist/types").CSSPropertiesWithVars | undefined;
95
- }, "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"> & {
96
102
  style?: import("react").CSSProperties | import("styled-components/dist/types").CSSPropertiesWithVars | undefined;
97
- }, "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;
@@ -172,13 +177,49 @@ export const FooterLink = styled.a `
172
177
  font-size: ${({ theme }) => theme.typography.fontSize.body};
173
178
  }
174
179
  `;
175
- export const FooterLegalMeta = styled.div `
176
- display: flex;
177
- flex-wrap: wrap;
180
+ /* A greyed column link: the link's type, dimmed, with no hover. */
181
+ export const FooterLinkDisabled = styled.span `
182
+ display: inline-flex;
178
183
  align-items: center;
179
- gap: 12px;
184
+ gap: 8px;
185
+ width: fit-content;
186
+ color: ${({ theme }) => theme.colors.ink};
187
+ font-size: 0.875rem;
188
+ font-weight: ${({ theme }) => theme.fontWeight.medium};
189
+ line-height: 1.5;
190
+ opacity: 0.5;
191
+ cursor: default;
180
192
 
181
193
  ${({ theme }) => theme.mediaQueries.lg} {
194
+ font-size: ${({ theme }) => theme.typography.fontSize.body};
195
+ }
196
+ `;
197
+ export const FooterLinkBadge = styled.span `
198
+ display: inline-flex;
199
+ align-items: center;
200
+ justify-content: center;
201
+ height: 18px;
202
+ padding: 0 8px;
203
+ border-radius: ${({ theme }) => theme.borderRadius.pill};
204
+ background: rgb(0 0 0 / 0.1);
205
+ color: currentColor;
206
+ font-size: 0.6875rem;
207
+ font-weight: ${({ theme }) => theme.fontWeight.black};
208
+ letter-spacing: 0.1em;
209
+ line-height: 1;
210
+ text-transform: uppercase;
211
+ white-space: nowrap;
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).
216
+ export const FooterLegalMeta = styled.div `
217
+ display: none;
218
+
219
+ ${({ theme }) => theme.mediaQueries.lg} {
220
+ display: flex;
221
+ flex-wrap: wrap;
222
+ align-items: center;
182
223
  gap: 10px 12px;
183
224
  }
184
225
  `;
@@ -227,30 +268,24 @@ export const FooterLegalLink = styled.a `
227
268
  line-height: 1.3333;
228
269
  }
229
270
  `;
230
- // The `!important`s below date from when this button carried OneTrust's
231
- // `#ot-sdk-btn.ot-sdk-show-settings` hook and had to beat the SDK's own
232
- // button styles. The hook is gone (see Footer/index.tsx); they are kept for
233
- // now so this change stays limited to the label/hydration fix.
234
271
  export const FooterLegalButton = styled.button `
235
- display: inline !important;
236
- min-width: 0 !important;
237
- min-height: 0 !important;
238
- padding: 0 !important;
239
- border: 0 !important;
240
- background: transparent !important;
241
- 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};
242
279
  cursor: pointer;
243
280
  font: inherit;
244
- font-size: 0.6875rem !important;
245
- font-weight: ${({ theme }) => theme.fontWeight.default} !important;
246
- line-height: 1.2727 !important;
247
- text-decoration: none !important;
248
- 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;
249
286
 
250
287
  &:hover {
251
- background: transparent !important;
252
- color: ${({ theme }) => theme.colors.footerTextMuted} !important;
253
- text-decoration: underline !important;
288
+ text-decoration: underline;
254
289
  }
255
290
 
256
291
  &:focus-visible {
@@ -259,8 +294,8 @@ export const FooterLegalButton = styled.button `
259
294
  }
260
295
 
261
296
  ${({ theme }) => theme.mediaQueries.lg} {
262
- font-size: 0.75rem !important;
263
- line-height: 1.3333 !important;
297
+ font-size: 0.75rem;
298
+ line-height: 1.3333;
264
299
  }
265
300
  `;
266
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,11 +1,15 @@
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;
5
- href: string;
5
+ /** Absent on a disabled item. */
6
+ href?: string | undefined;
6
7
  ariaLabel?: string | undefined;
7
8
  external?: boolean | undefined;
8
9
  onClick?: ((event: React.MouseEvent<HTMLAnchorElement>) => void) | undefined;
10
+ /** Greyed and non-interactive, with `badge` beside the label if given. */
11
+ disabled?: boolean | undefined;
12
+ badge?: string | undefined;
9
13
  }
10
14
  interface NavDropdownProps {
11
15
  label: string;