@cronos-labs/ui 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/FilterPillGroup/index.d.ts +1 -0
- package/dist/FilterPillGroup/index.js +1 -0
- package/dist/FilterPillGroup/styles.d.ts +8 -0
- package/dist/FilterPillGroup/styles.js +36 -0
- package/dist/Footer/index.d.ts +34 -0
- package/dist/Footer/index.js +48 -0
- package/dist/Footer/styles.d.ts +97 -0
- package/dist/Footer/styles.js +326 -0
- package/dist/Header/NavDropdown.d.ts +17 -0
- package/dist/Header/NavDropdown.js +39 -0
- package/dist/Header/helper.d.ts +1 -0
- package/dist/Header/helper.js +1 -0
- package/dist/Header/index.d.ts +46 -0
- package/dist/Header/index.js +162 -0
- package/dist/Header/styles.d.ts +158 -0
- package/dist/Header/styles.js +770 -0
- package/dist/LocaleSelector/index.d.ts +9 -0
- package/dist/LocaleSelector/index.js +62 -0
- package/dist/LocaleSelector/styles.d.ts +33 -0
- package/dist/LocaleSelector/styles.js +156 -0
- package/dist/SearchField/index.d.ts +14 -0
- package/dist/SearchField/index.js +6 -0
- package/dist/SearchField/styles.d.ts +20 -0
- package/dist/SearchField/styles.js +85 -0
- package/dist/SectionAnchorNav/index.d.ts +11 -0
- package/dist/SectionAnchorNav/index.js +41 -0
- package/dist/SectionAnchorNav/styles.d.ts +22 -0
- package/dist/SectionAnchorNav/styles.js +74 -0
- package/dist/Seo/index.d.ts +18 -0
- package/dist/Seo/index.js +53 -0
- package/dist/Seo/types.d.ts +4 -0
- package/dist/Seo/types.js +1 -0
- package/dist/config/eslintConfig.d.ts +6 -0
- package/dist/config/eslintConfig.js +31 -0
- package/dist/config/prettierConfig.d.ts +8 -0
- package/dist/config/prettierConfig.js +8 -0
- package/dist/format.d.ts +3 -0
- package/dist/format.js +9 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.js +18 -0
- package/dist/locale/config.d.ts +5 -0
- package/dist/locale/config.js +103 -0
- package/dist/locale/hooks.d.ts +3 -0
- package/dist/locale/hooks.js +36 -0
- package/dist/locale/localePreference.d.ts +7 -0
- package/dist/locale/localePreference.js +21 -0
- package/dist/locale/localization.d.ts +11 -0
- package/dist/locale/localization.js +12 -0
- package/dist/locale/routes.d.ts +5 -0
- package/dist/locale/routes.js +32 -0
- package/dist/locale/types.d.ts +25 -0
- package/dist/locale/types.js +1 -0
- package/dist/navigationContent.d.ts +59 -0
- package/dist/navigationContent.js +552 -0
- package/dist/scrollToSectionHeading.d.ts +1 -0
- package/dist/scrollToSectionHeading.js +12 -0
- package/dist/tokens.d.ts +252 -0
- package/dist/tokens.js +253 -0
- package/dist/typography.d.ts +11 -0
- package/dist/typography.js +106 -0
- package/eslint-config.js +1 -0
- package/package.json +98 -0
- package/prettier-config.js +1 -0
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
interface LocaleSelectorProps {
|
|
2
|
+
ariaLabel: string;
|
|
3
|
+
forceDocumentNavigation?: boolean;
|
|
4
|
+
inlineMenu?: boolean;
|
|
5
|
+
menuPlacement?: 'down' | 'up';
|
|
6
|
+
onLocaleSelect?: (() => void) | undefined;
|
|
7
|
+
}
|
|
8
|
+
export declare function LocaleSelector({ ariaLabel, forceDocumentNavigation, inlineMenu, menuPlacement, onLocaleSelect, }: LocaleSelectorProps): React.JSX.Element;
|
|
9
|
+
export {};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useRef, useState } from 'react';
|
|
3
|
+
import { useLocation, useNavigate } from 'react-router-dom';
|
|
4
|
+
import { localeConfig } from '../locale/config.js';
|
|
5
|
+
import { useCurrentLocale } from '../locale/hooks.js';
|
|
6
|
+
import { setStoredLocalePreference } from '../locale/localePreference.js';
|
|
7
|
+
import { buildLocalizedPath, stripLocalePrefix } from '../locale/routes.js';
|
|
8
|
+
import { LocaleSelectorButton, LocaleSelectorChevron, LocaleSelectorIcon, LocaleSelectorMenu, LocaleSelectorOption, LocaleSelectorOptionList, LocaleSelectorRoot, LocaleSelectorText, } from './styles.js';
|
|
9
|
+
export function LocaleSelector({ ariaLabel, forceDocumentNavigation = false, inlineMenu = false, menuPlacement = 'up', onLocaleSelect, }) {
|
|
10
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
11
|
+
const selectorRef = useRef(null);
|
|
12
|
+
const locale = useCurrentLocale();
|
|
13
|
+
const location = useLocation();
|
|
14
|
+
const navigate = useNavigate();
|
|
15
|
+
const menuId = 'footer-locale-menu';
|
|
16
|
+
const currentLocaleLabel = localeConfig.locales[locale].selectorLabel;
|
|
17
|
+
const localeOptions = localeConfig.supportedLocales.map((optionLocale) => ({
|
|
18
|
+
isCurrent: optionLocale === locale,
|
|
19
|
+
label: localeConfig.locales[optionLocale].selectorLabel,
|
|
20
|
+
locale: optionLocale,
|
|
21
|
+
}));
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
setIsOpen(false);
|
|
24
|
+
}, [location.pathname, location.hash]);
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
if (!isOpen)
|
|
27
|
+
return;
|
|
28
|
+
const handlePointerDown = (event) => {
|
|
29
|
+
if (event.target instanceof Node && selectorRef.current?.contains(event.target)) {
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
setIsOpen(false);
|
|
33
|
+
};
|
|
34
|
+
const handleKeyDown = (event) => {
|
|
35
|
+
if (event.key === 'Escape') {
|
|
36
|
+
setIsOpen(false);
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
document.addEventListener('pointerdown', handlePointerDown);
|
|
40
|
+
document.addEventListener('keydown', handleKeyDown);
|
|
41
|
+
return () => {
|
|
42
|
+
document.removeEventListener('pointerdown', handlePointerDown);
|
|
43
|
+
document.removeEventListener('keydown', handleKeyDown);
|
|
44
|
+
};
|
|
45
|
+
}, [isOpen]);
|
|
46
|
+
const buildLocaleTarget = (targetLocale) => {
|
|
47
|
+
const targetPathname = buildLocalizedPath(stripLocalePrefix(location.pathname), targetLocale);
|
|
48
|
+
return `${targetPathname}${location.search}${location.hash}`;
|
|
49
|
+
};
|
|
50
|
+
const handleLocaleSelect = (targetLocale) => {
|
|
51
|
+
const targetHref = buildLocaleTarget(targetLocale);
|
|
52
|
+
setStoredLocalePreference(targetLocale);
|
|
53
|
+
setIsOpen(false);
|
|
54
|
+
onLocaleSelect?.();
|
|
55
|
+
if (forceDocumentNavigation) {
|
|
56
|
+
window.location.assign(targetHref);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
void navigate(targetHref);
|
|
60
|
+
};
|
|
61
|
+
return (_jsxs(LocaleSelectorRoot, { ref: selectorRef, "$inlineMenu": inlineMenu, children: [_jsxs(LocaleSelectorButton, { type: "button", "aria-label": ariaLabel, "aria-haspopup": "menu", "aria-expanded": isOpen, "aria-controls": menuId, onClick: () => setIsOpen((currentValue) => !currentValue), children: [_jsx(LocaleSelectorIcon, { "aria-hidden": "true" }), _jsx(LocaleSelectorText, { children: currentLocaleLabel }), _jsx(LocaleSelectorChevron, { "$open": isOpen, "aria-hidden": "true" })] }), isOpen ? (_jsx(LocaleSelectorMenu, { id: menuId, role: "menu", "aria-label": "Language options", "$inline": inlineMenu, "$placement": menuPlacement, children: _jsx(LocaleSelectorOptionList, { children: localeOptions.map((option) => (_jsx(LocaleSelectorOption, { type: "button", role: "menuitemradio", "aria-checked": option.isCurrent, "$active": option.isCurrent, onClick: () => handleLocaleSelect(option.locale), children: _jsx("span", { children: option.label }) }, option.label))) }) })) : null] }));
|
|
62
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export declare const LocaleSelectorRoot: import("styled-components/dist/types").IStyledComponentBase<"web", import("styled-components/dist/types").Merged<Omit<import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLDivElement>, HTMLDivElement>, "style"> & {
|
|
2
|
+
style?: import("react").CSSProperties | import("styled-components/dist/types").CSSPropertiesWithVars | undefined;
|
|
3
|
+
}, {
|
|
4
|
+
$inlineMenu: boolean;
|
|
5
|
+
}>> & string;
|
|
6
|
+
export declare const LocaleSelectorButton: import("styled-components/dist/types").IStyledComponentBase<"web", Omit<import("react").DetailedHTMLProps<import("react").ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>, "style"> & {
|
|
7
|
+
style?: import("react").CSSProperties | import("styled-components/dist/types").CSSPropertiesWithVars | undefined;
|
|
8
|
+
}> & string;
|
|
9
|
+
export declare const LocaleSelectorIcon: import("styled-components/dist/types").IStyledComponentBase<"web", Omit<import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLSpanElement>, HTMLSpanElement>, "style"> & {
|
|
10
|
+
style?: import("react").CSSProperties | import("styled-components/dist/types").CSSPropertiesWithVars | undefined;
|
|
11
|
+
}> & string;
|
|
12
|
+
export declare const LocaleSelectorText: import("styled-components/dist/types").IStyledComponentBase<"web", Omit<import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLSpanElement>, HTMLSpanElement>, "style"> & {
|
|
13
|
+
style?: import("react").CSSProperties | import("styled-components/dist/types").CSSPropertiesWithVars | undefined;
|
|
14
|
+
}> & string;
|
|
15
|
+
export declare const LocaleSelectorChevron: import("styled-components/dist/types").IStyledComponentBase<"web", import("styled-components/dist/types").Merged<Omit<import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLSpanElement>, HTMLSpanElement>, "style"> & {
|
|
16
|
+
style?: import("react").CSSProperties | import("styled-components/dist/types").CSSPropertiesWithVars | undefined;
|
|
17
|
+
}, {
|
|
18
|
+
$open: boolean;
|
|
19
|
+
}>> & string;
|
|
20
|
+
export declare const LocaleSelectorMenu: import("styled-components/dist/types").IStyledComponentBase<"web", import("styled-components/dist/types").Merged<Omit<import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLDivElement>, HTMLDivElement>, "style"> & {
|
|
21
|
+
style?: import("react").CSSProperties | import("styled-components/dist/types").CSSPropertiesWithVars | undefined;
|
|
22
|
+
}, {
|
|
23
|
+
$inline: boolean;
|
|
24
|
+
$placement: "down" | "up";
|
|
25
|
+
}>> & string;
|
|
26
|
+
export declare const LocaleSelectorOptionList: import("styled-components/dist/types").IStyledComponentBase<"web", Omit<import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLDivElement>, HTMLDivElement>, "style"> & {
|
|
27
|
+
style?: import("react").CSSProperties | import("styled-components/dist/types").CSSPropertiesWithVars | undefined;
|
|
28
|
+
}> & string;
|
|
29
|
+
export declare const LocaleSelectorOption: import("styled-components/dist/types").IStyledComponentBase<"web", import("styled-components/dist/types").Merged<Omit<import("react").DetailedHTMLProps<import("react").ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>, "style"> & {
|
|
30
|
+
style?: import("react").CSSProperties | import("styled-components/dist/types").CSSPropertiesWithVars | undefined;
|
|
31
|
+
}, {
|
|
32
|
+
$active: boolean;
|
|
33
|
+
}>> & string;
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import styled from 'styled-components';
|
|
2
|
+
export const LocaleSelectorRoot = styled.div `
|
|
3
|
+
position: relative;
|
|
4
|
+
display: inline-flex;
|
|
5
|
+
flex-direction: ${({ $inlineMenu }) => ($inlineMenu ? 'column' : 'row')};
|
|
6
|
+
gap: ${({ $inlineMenu }) => ($inlineMenu ? '8px' : '0')};
|
|
7
|
+
width: min(100%, 331px);
|
|
8
|
+
|
|
9
|
+
${({ theme }) => theme.mediaQueries.lg} {
|
|
10
|
+
width: 197px;
|
|
11
|
+
}
|
|
12
|
+
`;
|
|
13
|
+
export const LocaleSelectorButton = styled.button `
|
|
14
|
+
display: inline-flex;
|
|
15
|
+
align-items: center;
|
|
16
|
+
justify-content: flex-start;
|
|
17
|
+
gap: 10px;
|
|
18
|
+
width: 100%;
|
|
19
|
+
height: 44px;
|
|
20
|
+
padding: 9px 8px;
|
|
21
|
+
border: 1px solid #e5faff;
|
|
22
|
+
border-radius: 8px;
|
|
23
|
+
background: ${({ theme }) => theme.colors.brandSky};
|
|
24
|
+
color: ${({ theme }) => theme.colors.ink};
|
|
25
|
+
cursor: pointer;
|
|
26
|
+
transition: opacity ${({ theme }) => theme.motion.duration.fast}
|
|
27
|
+
${({ theme }) => theme.motion.easing.standard};
|
|
28
|
+
|
|
29
|
+
&:hover {
|
|
30
|
+
opacity: 0.96;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
&:focus-visible {
|
|
34
|
+
outline: 2px solid ${({ theme }) => theme.colors.ink};
|
|
35
|
+
outline-offset: 3px;
|
|
36
|
+
}
|
|
37
|
+
`;
|
|
38
|
+
export const LocaleSelectorIcon = styled.span `
|
|
39
|
+
position: relative;
|
|
40
|
+
width: 24px;
|
|
41
|
+
height: 24px;
|
|
42
|
+
flex: 0 0 auto;
|
|
43
|
+
border: 2px solid currentColor;
|
|
44
|
+
border-radius: 999px;
|
|
45
|
+
|
|
46
|
+
&::before,
|
|
47
|
+
&::after {
|
|
48
|
+
content: '';
|
|
49
|
+
position: absolute;
|
|
50
|
+
inset: 50% auto auto 50%;
|
|
51
|
+
transform: translate(-50%, -50%);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
&::before {
|
|
55
|
+
width: 7px;
|
|
56
|
+
height: 18px;
|
|
57
|
+
border-right: 2px solid currentColor;
|
|
58
|
+
border-left: 2px solid currentColor;
|
|
59
|
+
border-radius: 999px;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
&::after {
|
|
63
|
+
width: 18px;
|
|
64
|
+
height: 2px;
|
|
65
|
+
background: currentColor;
|
|
66
|
+
box-shadow:
|
|
67
|
+
0 -5px 0 currentColor,
|
|
68
|
+
0 5px 0 currentColor;
|
|
69
|
+
}
|
|
70
|
+
`;
|
|
71
|
+
export const LocaleSelectorText = styled.span `
|
|
72
|
+
min-width: 0;
|
|
73
|
+
flex: 1;
|
|
74
|
+
color: inherit;
|
|
75
|
+
font-size: ${({ theme }) => theme.typography.fontSize.bodySm};
|
|
76
|
+
font-weight: ${({ theme }) => theme.fontWeight.bold};
|
|
77
|
+
letter-spacing: 0;
|
|
78
|
+
line-height: 18px;
|
|
79
|
+
overflow: hidden;
|
|
80
|
+
text-align: left;
|
|
81
|
+
text-overflow: ellipsis;
|
|
82
|
+
white-space: nowrap;
|
|
83
|
+
`;
|
|
84
|
+
export const LocaleSelectorChevron = styled.span `
|
|
85
|
+
position: relative;
|
|
86
|
+
width: 24px;
|
|
87
|
+
height: 24px;
|
|
88
|
+
flex: 0 0 auto;
|
|
89
|
+
|
|
90
|
+
&::before {
|
|
91
|
+
content: '';
|
|
92
|
+
position: absolute;
|
|
93
|
+
left: 7px;
|
|
94
|
+
top: ${({ $open }) => ($open ? '9px' : '7px')};
|
|
95
|
+
width: 9px;
|
|
96
|
+
height: 9px;
|
|
97
|
+
border-right: 2px solid currentColor;
|
|
98
|
+
border-bottom: 2px solid currentColor;
|
|
99
|
+
transform: ${({ $open }) => ($open ? 'rotate(225deg)' : 'rotate(45deg)')};
|
|
100
|
+
transform-origin: center;
|
|
101
|
+
}
|
|
102
|
+
`;
|
|
103
|
+
export const LocaleSelectorMenu = styled.div `
|
|
104
|
+
position: ${({ $inline }) => ($inline ? 'static' : 'absolute')};
|
|
105
|
+
${({ $inline, $placement }) => $inline ? '' : $placement === 'down' ? 'top: calc(100% + 8px);' : 'bottom: calc(100% + 8px);'}
|
|
106
|
+
left: ${({ $inline }) => ($inline ? 'auto' : '0')};
|
|
107
|
+
z-index: ${({ theme }) => theme.zIndex.overlay};
|
|
108
|
+
width: ${({ $inline }) => ($inline ? '100%' : 'min(100vw - 32px, 331px)')};
|
|
109
|
+
padding: 4px;
|
|
110
|
+
border: 1px solid #f2f2f3;
|
|
111
|
+
border-radius: 12px;
|
|
112
|
+
background: ${({ theme }) => theme.colors.light};
|
|
113
|
+
color: ${({ theme }) => theme.colors.ink};
|
|
114
|
+
box-shadow:
|
|
115
|
+
0 15px 20px -5px rgb(38 38 38 / 0.1),
|
|
116
|
+
0 5px 10px -5px rgb(38 38 38 / 0.05);
|
|
117
|
+
|
|
118
|
+
${({ theme }) => theme.mediaQueries.lg} {
|
|
119
|
+
width: ${({ $inline }) => ($inline ? '100%' : '232px')};
|
|
120
|
+
}
|
|
121
|
+
`;
|
|
122
|
+
export const LocaleSelectorOptionList = styled.div `
|
|
123
|
+
display: grid;
|
|
124
|
+
gap: 2px;
|
|
125
|
+
`;
|
|
126
|
+
export const LocaleSelectorOption = styled.button `
|
|
127
|
+
display: flex;
|
|
128
|
+
align-items: center;
|
|
129
|
+
gap: 8px;
|
|
130
|
+
width: 100%;
|
|
131
|
+
height: 44px;
|
|
132
|
+
padding: 12px;
|
|
133
|
+
border: 0;
|
|
134
|
+
border-radius: 8px;
|
|
135
|
+
background: ${({ $active, theme }) => ($active ? theme.colors.brandSky : 'transparent')};
|
|
136
|
+
color: inherit;
|
|
137
|
+
cursor: pointer;
|
|
138
|
+
font-size: ${({ theme }) => theme.typography.fontSize.bodySm};
|
|
139
|
+
font-weight: ${({ theme }) => theme.fontWeight.black};
|
|
140
|
+
line-height: 1.4;
|
|
141
|
+
text-align: left;
|
|
142
|
+
|
|
143
|
+
&:hover {
|
|
144
|
+
background: ${({ theme }) => theme.colors.brandSky};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
&:disabled {
|
|
148
|
+
cursor: default;
|
|
149
|
+
opacity: 1;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
&:focus-visible {
|
|
153
|
+
outline: 2px solid currentColor;
|
|
154
|
+
outline-offset: -3px;
|
|
155
|
+
}
|
|
156
|
+
`;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface SearchFieldProps {
|
|
2
|
+
label: string;
|
|
3
|
+
placeholder: string;
|
|
4
|
+
value: string;
|
|
5
|
+
onChange: (value: string) => void;
|
|
6
|
+
/** Resolved icon URL — each caller resolves the asset its own bundler's way (e.g. Astro's `.src`, Vite's plain import) and passes the string in. */
|
|
7
|
+
iconSrc: string;
|
|
8
|
+
/** Renders a clear button once the field has a value; omit for a field with no clear affordance. */
|
|
9
|
+
onClear?: () => void;
|
|
10
|
+
clearLabel?: string;
|
|
11
|
+
/** Result-count or status text shown beneath the field. */
|
|
12
|
+
hint?: string | null;
|
|
13
|
+
}
|
|
14
|
+
export declare function SearchField({ label, placeholder, value, onChange, iconSrc, onClear, clearLabel, hint, }: SearchFieldProps): React.JSX.Element;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { ClearButton, Column, Hint, Icon, Input, Root } from './styles.js';
|
|
3
|
+
export function SearchField({ label, placeholder, value, onChange, iconSrc, onClear, clearLabel, hint, }) {
|
|
4
|
+
const showsClearButton = onClear !== undefined && value.length > 0;
|
|
5
|
+
return (_jsxs(Column, { children: [_jsxs(Root, { children: [_jsx(Icon, { src: iconSrc, alt: "", "aria-hidden": "true" }), _jsx(Input, { type: "search", value: value, onChange: (event) => onChange(event.target.value), placeholder: placeholder, "aria-label": label, "$hasClearButton": showsClearButton }), showsClearButton && (_jsx(ClearButton, { type: "button", "aria-label": clearLabel, onClick: onClear, children: _jsx("svg", { width: "12", height: "12", viewBox: "0 0 12 12", fill: "none", stroke: "currentColor", strokeWidth: "1.8", strokeLinecap: "round", "aria-hidden": "true", focusable: "false", children: _jsx("path", { d: "M2.5 2.5 L9.5 9.5 M9.5 2.5 L2.5 9.5" }) }) }))] }), hint !== null && hint !== undefined && _jsx(Hint, { role: "status", children: hint })] }));
|
|
6
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export declare const Column: import("styled-components/dist/types").IStyledComponentBase<"web", Omit<import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLDivElement>, HTMLDivElement>, "style"> & {
|
|
2
|
+
style?: import("react").CSSProperties | import("styled-components/dist/types").CSSPropertiesWithVars | undefined;
|
|
3
|
+
}> & string;
|
|
4
|
+
export declare const Root: import("styled-components/dist/types").IStyledComponentBase<"web", Omit<import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLDivElement>, HTMLDivElement>, "style"> & {
|
|
5
|
+
style?: import("react").CSSProperties | import("styled-components/dist/types").CSSPropertiesWithVars | undefined;
|
|
6
|
+
}> & string;
|
|
7
|
+
export declare const Icon: import("styled-components/dist/types").IStyledComponentBase<"web", Omit<import("react").DetailedHTMLProps<import("react").ImgHTMLAttributes<HTMLImageElement>, HTMLImageElement>, "style"> & {
|
|
8
|
+
style?: import("react").CSSProperties | import("styled-components/dist/types").CSSPropertiesWithVars | undefined;
|
|
9
|
+
}> & string;
|
|
10
|
+
export declare const Input: import("styled-components/dist/types").IStyledComponentBase<"web", import("styled-components/dist/types").Merged<Omit<import("react").DetailedHTMLProps<import("react").InputHTMLAttributes<HTMLInputElement>, HTMLInputElement>, "style"> & {
|
|
11
|
+
style?: import("react").CSSProperties | import("styled-components/dist/types").CSSPropertiesWithVars | undefined;
|
|
12
|
+
}, {
|
|
13
|
+
$hasClearButton: boolean;
|
|
14
|
+
}>> & string;
|
|
15
|
+
export declare const ClearButton: import("styled-components/dist/types").IStyledComponentBase<"web", Omit<import("react").DetailedHTMLProps<import("react").ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>, "style"> & {
|
|
16
|
+
style?: import("react").CSSProperties | import("styled-components/dist/types").CSSPropertiesWithVars | undefined;
|
|
17
|
+
}> & string;
|
|
18
|
+
export declare const Hint: import("styled-components/dist/types").IStyledComponentBase<"web", Omit<import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLParagraphElement>, HTMLParagraphElement>, "style"> & {
|
|
19
|
+
style?: import("react").CSSProperties | import("styled-components/dist/types").CSSPropertiesWithVars | undefined;
|
|
20
|
+
}> & string;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import styled from 'styled-components';
|
|
2
|
+
import { metaTypography } from '../typography.js';
|
|
3
|
+
export const Column = styled.div `
|
|
4
|
+
display: flex;
|
|
5
|
+
flex-direction: column;
|
|
6
|
+
gap: ${({ theme }) => theme.sizes.xxxs};
|
|
7
|
+
min-width: 0;
|
|
8
|
+
`;
|
|
9
|
+
export const Root = styled.div `
|
|
10
|
+
position: relative;
|
|
11
|
+
display: flex;
|
|
12
|
+
align-items: center;
|
|
13
|
+
width: 100%;
|
|
14
|
+
max-width: 360px;
|
|
15
|
+
`;
|
|
16
|
+
export const Icon = styled.img `
|
|
17
|
+
position: absolute;
|
|
18
|
+
left: 14px;
|
|
19
|
+
display: block;
|
|
20
|
+
width: 16px;
|
|
21
|
+
height: 16px;
|
|
22
|
+
object-fit: contain;
|
|
23
|
+
opacity: 0.5;
|
|
24
|
+
pointer-events: none;
|
|
25
|
+
`;
|
|
26
|
+
// Right padding matches the left icon gutter only once a clear button needs
|
|
27
|
+
// the room — the common (empty) state stays a 1:1 match with the field's
|
|
28
|
+
// base padding instead of always reserving space nothing sits in.
|
|
29
|
+
export const Input = styled.input `
|
|
30
|
+
width: 100%;
|
|
31
|
+
padding: ${({ $hasClearButton }) => $hasClearButton ? '10px 40px 10px 40px' : '10px 14px 10px 40px'};
|
|
32
|
+
border-radius: ${({ theme }) => theme.borderRadius.pill};
|
|
33
|
+
border: 1px solid ${({ theme }) => theme.colors.borderDarkSubtle};
|
|
34
|
+
background: ${({ theme }) => theme.colors.surfaceDarkElevated};
|
|
35
|
+
color: ${({ theme }) => theme.colors.light};
|
|
36
|
+
font-family: inherit;
|
|
37
|
+
font-size: ${({ theme }) => theme.typography.fontSize.footnote};
|
|
38
|
+
|
|
39
|
+
&::placeholder {
|
|
40
|
+
color: ${({ theme }) => theme.colors.overlayTextMuted};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/* The native search affordances are replaced by the designed clear button. */
|
|
44
|
+
&::-webkit-search-cancel-button {
|
|
45
|
+
appearance: none;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
&:focus-visible {
|
|
49
|
+
outline: none;
|
|
50
|
+
box-shadow: 0 0 0 3px ${({ theme }) => theme.colors.focusRing};
|
|
51
|
+
}
|
|
52
|
+
`;
|
|
53
|
+
export const ClearButton = styled.button `
|
|
54
|
+
${metaTypography}
|
|
55
|
+
position: absolute;
|
|
56
|
+
right: ${({ theme }) => theme.sizes.xxs};
|
|
57
|
+
display: flex;
|
|
58
|
+
align-items: center;
|
|
59
|
+
justify-content: center;
|
|
60
|
+
width: ${({ theme }) => theme.sizes.lg};
|
|
61
|
+
height: ${({ theme }) => theme.sizes.lg};
|
|
62
|
+
padding: 0;
|
|
63
|
+
border: none;
|
|
64
|
+
border-radius: ${({ theme }) => theme.borderRadius.pill};
|
|
65
|
+
background: ${({ theme }) => theme.colors.overlaySurfaceStrong};
|
|
66
|
+
color: ${({ theme }) => theme.colors.overlayTextSoft};
|
|
67
|
+
line-height: 1;
|
|
68
|
+
cursor: pointer;
|
|
69
|
+
|
|
70
|
+
&:hover {
|
|
71
|
+
background: ${({ theme }) => theme.colors.overlayBorderSubtle};
|
|
72
|
+
color: ${({ theme }) => theme.colors.light};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
&:focus-visible {
|
|
76
|
+
outline: none;
|
|
77
|
+
box-shadow: 0 0 0 3px ${({ theme }) => theme.colors.focusRing};
|
|
78
|
+
}
|
|
79
|
+
`;
|
|
80
|
+
export const Hint = styled.p `
|
|
81
|
+
${metaTypography}
|
|
82
|
+
margin: 0;
|
|
83
|
+
padding-left: ${({ theme }) => theme.sizes.xs};
|
|
84
|
+
color: ${({ theme }) => theme.colors.overlayTextSoft};
|
|
85
|
+
`;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
export interface SectionAnchorNavItem {
|
|
3
|
+
href: `#${string}`;
|
|
4
|
+
label: ReactNode;
|
|
5
|
+
}
|
|
6
|
+
export declare function SectionAnchorNav({ ariaLabel, initialActiveHref, items, markerSrc, }: {
|
|
7
|
+
ariaLabel: string;
|
|
8
|
+
initialActiveHref: `#${string}`;
|
|
9
|
+
items: readonly SectionAnchorNavItem[];
|
|
10
|
+
markerSrc?: string;
|
|
11
|
+
}): React.JSX.Element;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useState } from 'react';
|
|
3
|
+
import { scrollToSectionHeading } from '../scrollToSectionHeading.js';
|
|
4
|
+
import { List, MarkerImage, MarkerShape, MarkerSlot, Nav, NavLink } from './styles.js';
|
|
5
|
+
function handleNavLinkClick(event, href) {
|
|
6
|
+
if (!href.startsWith('#'))
|
|
7
|
+
return;
|
|
8
|
+
event.preventDefault();
|
|
9
|
+
scrollToSectionHeading(href.slice(1));
|
|
10
|
+
window.history.pushState(null, '', href);
|
|
11
|
+
}
|
|
12
|
+
export function SectionAnchorNav({ ariaLabel, initialActiveHref, items, markerSrc, }) {
|
|
13
|
+
const [activeHref, setActiveHref] = useState(initialActiveHref);
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
if (items.length === 0)
|
|
16
|
+
return undefined;
|
|
17
|
+
const sectionIds = items.map((item) => item.href.slice(1));
|
|
18
|
+
const updateActiveSection = () => {
|
|
19
|
+
const viewportAnchor = window.innerHeight * 0.35;
|
|
20
|
+
let currentSectionId = sectionIds[0];
|
|
21
|
+
sectionIds.forEach((sectionId) => {
|
|
22
|
+
const section = document.getElementById(sectionId);
|
|
23
|
+
if (section && section.getBoundingClientRect().top <= viewportAnchor) {
|
|
24
|
+
currentSectionId = sectionId;
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
setActiveHref(`#${currentSectionId}`);
|
|
28
|
+
};
|
|
29
|
+
updateActiveSection();
|
|
30
|
+
window.addEventListener('scroll', updateActiveSection, { passive: true });
|
|
31
|
+
window.addEventListener('resize', updateActiveSection);
|
|
32
|
+
return () => {
|
|
33
|
+
window.removeEventListener('scroll', updateActiveSection);
|
|
34
|
+
window.removeEventListener('resize', updateActiveSection);
|
|
35
|
+
};
|
|
36
|
+
}, [items]);
|
|
37
|
+
return (_jsx(Nav, { "aria-label": ariaLabel, children: _jsx(List, { children: items.map((item) => {
|
|
38
|
+
const isActive = activeHref === item.href;
|
|
39
|
+
return (_jsx("li", { children: _jsxs(NavLink, { href: item.href, "$active": isActive, "aria-current": isActive ? 'location' : undefined, onClick: (event) => handleNavLinkClick(event, item.href), children: [_jsxs(MarkerSlot, { "$active": isActive, children: [markerSrc ? _jsx(MarkerImage, { src: markerSrc, alt: "", "aria-hidden": "true" }) : null, markerSrc ? null : _jsx(MarkerShape, { "aria-hidden": "true" })] }), item.label] }) }, item.href));
|
|
40
|
+
}) }) }));
|
|
41
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export declare const Nav: import("styled-components/dist/types").IStyledComponentBase<"web", Omit<import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLElement>, HTMLElement>, "style"> & {
|
|
2
|
+
style?: import("react").CSSProperties | import("styled-components/dist/types").CSSPropertiesWithVars | undefined;
|
|
3
|
+
}> & string;
|
|
4
|
+
export declare const List: import("styled-components/dist/types").IStyledComponentBase<"web", Omit<import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLUListElement>, HTMLUListElement>, "style"> & {
|
|
5
|
+
style?: import("react").CSSProperties | import("styled-components/dist/types").CSSPropertiesWithVars | undefined;
|
|
6
|
+
}> & string;
|
|
7
|
+
export declare const NavLink: import("styled-components/dist/types").IStyledComponentBase<"web", import("styled-components/dist/types").Merged<Omit<import("react").DetailedHTMLProps<import("react").AnchorHTMLAttributes<HTMLAnchorElement>, HTMLAnchorElement>, "style"> & {
|
|
8
|
+
style?: import("react").CSSProperties | import("styled-components/dist/types").CSSPropertiesWithVars | undefined;
|
|
9
|
+
}, {
|
|
10
|
+
$active?: boolean;
|
|
11
|
+
}>> & string;
|
|
12
|
+
export declare const MarkerSlot: import("styled-components/dist/types").IStyledComponentBase<"web", import("styled-components/dist/types").Merged<Omit<import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLSpanElement>, HTMLSpanElement>, "style"> & {
|
|
13
|
+
style?: import("react").CSSProperties | import("styled-components/dist/types").CSSPropertiesWithVars | undefined;
|
|
14
|
+
}, {
|
|
15
|
+
$active?: boolean;
|
|
16
|
+
}>> & string;
|
|
17
|
+
export declare const MarkerImage: import("styled-components/dist/types").IStyledComponentBase<"web", Omit<import("react").DetailedHTMLProps<import("react").ImgHTMLAttributes<HTMLImageElement>, HTMLImageElement>, "style"> & {
|
|
18
|
+
style?: import("react").CSSProperties | import("styled-components/dist/types").CSSPropertiesWithVars | undefined;
|
|
19
|
+
}> & string;
|
|
20
|
+
export declare const MarkerShape: import("styled-components/dist/types").IStyledComponentBase<"web", Omit<import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLSpanElement>, HTMLSpanElement>, "style"> & {
|
|
21
|
+
style?: import("react").CSSProperties | import("styled-components/dist/types").CSSPropertiesWithVars | undefined;
|
|
22
|
+
}> & string;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import styled from 'styled-components';
|
|
2
|
+
export const Nav = styled.nav `
|
|
3
|
+
display: none;
|
|
4
|
+
|
|
5
|
+
${({ theme }) => theme.mediaQueries.lg} {
|
|
6
|
+
display: grid;
|
|
7
|
+
gap: 8px;
|
|
8
|
+
position: sticky;
|
|
9
|
+
top: clamp(88px, 18vh, 144px);
|
|
10
|
+
align-self: start;
|
|
11
|
+
width: 199px;
|
|
12
|
+
max-height: calc(100vh - 176px);
|
|
13
|
+
padding: 22px 25px;
|
|
14
|
+
border-radius: ${({ theme }) => theme.borderRadius.cardLg};
|
|
15
|
+
background: ${({ theme }) => theme.colors.overlaySurfaceSubtle};
|
|
16
|
+
overflow: auto;
|
|
17
|
+
scrollbar-width: none;
|
|
18
|
+
|
|
19
|
+
&::-webkit-scrollbar {
|
|
20
|
+
display: none;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
`;
|
|
24
|
+
export const List = styled.ul `
|
|
25
|
+
display: grid;
|
|
26
|
+
gap: 8px;
|
|
27
|
+
margin: 0;
|
|
28
|
+
padding: 0;
|
|
29
|
+
list-style: none;
|
|
30
|
+
`;
|
|
31
|
+
export const NavLink = styled.a `
|
|
32
|
+
display: inline-flex;
|
|
33
|
+
align-items: flex-start;
|
|
34
|
+
gap: 6px;
|
|
35
|
+
width: fit-content;
|
|
36
|
+
margin-inline: -8px;
|
|
37
|
+
padding: 4px 8px;
|
|
38
|
+
color: ${({ theme, $active }) => ($active ? theme.colors.brandSky : theme.colors.light)};
|
|
39
|
+
font-size: 0.875rem;
|
|
40
|
+
line-height: 1.4286;
|
|
41
|
+
font-weight: ${({ theme, $active }) => $active ? theme.fontWeight.extrabold : theme.fontWeight.medium};
|
|
42
|
+
text-decoration: none;
|
|
43
|
+
transition: color ${({ theme }) => theme.motion.duration.fast}
|
|
44
|
+
${({ theme }) => theme.motion.easing.standard};
|
|
45
|
+
|
|
46
|
+
&:hover {
|
|
47
|
+
color: ${({ theme }) => theme.colors.brandSky};
|
|
48
|
+
}
|
|
49
|
+
`;
|
|
50
|
+
export const MarkerSlot = styled.span `
|
|
51
|
+
display: block;
|
|
52
|
+
width: 9px;
|
|
53
|
+
height: 9px;
|
|
54
|
+
flex: 0 0 auto;
|
|
55
|
+
margin-top: 0.375rem;
|
|
56
|
+
opacity: ${({ $active }) => ($active ? 1 : 0)};
|
|
57
|
+
pointer-events: none;
|
|
58
|
+
transition: opacity ${({ theme }) => theme.motion.duration.fast}
|
|
59
|
+
${({ theme }) => theme.motion.easing.standard};
|
|
60
|
+
`;
|
|
61
|
+
export const MarkerImage = styled.img `
|
|
62
|
+
display: block;
|
|
63
|
+
width: 9px;
|
|
64
|
+
height: 9px;
|
|
65
|
+
filter: brightness(0) saturate(100%) invert(82%) sepia(97%) saturate(1253%) hue-rotate(155deg)
|
|
66
|
+
brightness(104%) contrast(101%);
|
|
67
|
+
`;
|
|
68
|
+
export const MarkerShape = styled.span `
|
|
69
|
+
display: block;
|
|
70
|
+
width: 9px;
|
|
71
|
+
height: 9px;
|
|
72
|
+
background: ${({ theme }) => theme.colors.brandSky};
|
|
73
|
+
clip-path: polygon(50% 0, 64% 36%, 100% 50%, 64% 64%, 50% 100%, 36% 64%, 0 50%, 36% 36%);
|
|
74
|
+
`;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { SeoMeta } from './types.js';
|
|
2
|
+
export interface RouteSeoMeta {
|
|
3
|
+
canonicalUrl: string;
|
|
4
|
+
description: string;
|
|
5
|
+
htmlLang: string;
|
|
6
|
+
imageAlt: string;
|
|
7
|
+
imageUrl: string;
|
|
8
|
+
ogLocale: string;
|
|
9
|
+
ogType: 'article' | 'website';
|
|
10
|
+
robots: string;
|
|
11
|
+
themeColor: string;
|
|
12
|
+
title: string;
|
|
13
|
+
}
|
|
14
|
+
interface SeoProps extends SeoMeta {
|
|
15
|
+
resolveRouteMeta?: ((pathname: string) => RouteSeoMeta | null) | undefined;
|
|
16
|
+
}
|
|
17
|
+
export declare function Seo({ title, description, resolveRouteMeta }: SeoProps): null;
|
|
18
|
+
export {};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { useEffect } from 'react';
|
|
2
|
+
// These create the tag when it is absent rather than skipping it. Updating only
|
|
3
|
+
// pre-existing nodes makes a tag missing from the host document a silent no-op,
|
|
4
|
+
// which is how a consuming app can ship with no og:image at all and no error.
|
|
5
|
+
const upsertMeta = (keyName, keyValue, content) => {
|
|
6
|
+
const existing = document.querySelector(`meta[${keyName}="${keyValue}"]`);
|
|
7
|
+
const node = existing ?? document.createElement('meta');
|
|
8
|
+
if (existing === null) {
|
|
9
|
+
node.setAttribute(keyName, keyValue);
|
|
10
|
+
document.head.appendChild(node);
|
|
11
|
+
}
|
|
12
|
+
node.setAttribute('content', content);
|
|
13
|
+
};
|
|
14
|
+
const upsertLink = (rel, href) => {
|
|
15
|
+
const existing = document.querySelector(`link[rel="${rel}"]`);
|
|
16
|
+
const node = existing ?? document.createElement('link');
|
|
17
|
+
if (existing === null) {
|
|
18
|
+
node.setAttribute('rel', rel);
|
|
19
|
+
document.head.appendChild(node);
|
|
20
|
+
}
|
|
21
|
+
node.setAttribute('href', href);
|
|
22
|
+
};
|
|
23
|
+
export function Seo({ title, description, resolveRouteMeta }) {
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
const routeSeo = typeof window === 'undefined' || !resolveRouteMeta
|
|
26
|
+
? null
|
|
27
|
+
: resolveRouteMeta(window.location.pathname);
|
|
28
|
+
const resolvedTitle = routeSeo?.title ?? title;
|
|
29
|
+
const resolvedDescription = routeSeo?.description ?? description;
|
|
30
|
+
document.title = resolvedTitle;
|
|
31
|
+
upsertMeta('name', 'description', resolvedDescription);
|
|
32
|
+
if (!routeSeo) {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
document.documentElement.lang = routeSeo.htmlLang;
|
|
36
|
+
upsertMeta('name', 'robots', routeSeo.robots);
|
|
37
|
+
upsertMeta('name', 'theme-color', routeSeo.themeColor);
|
|
38
|
+
upsertMeta('property', 'og:title', routeSeo.title);
|
|
39
|
+
upsertMeta('property', 'og:description', routeSeo.description);
|
|
40
|
+
upsertMeta('property', 'og:url', routeSeo.canonicalUrl);
|
|
41
|
+
upsertMeta('property', 'og:type', routeSeo.ogType);
|
|
42
|
+
upsertMeta('property', 'og:locale', routeSeo.ogLocale);
|
|
43
|
+
upsertMeta('property', 'og:image', routeSeo.imageUrl);
|
|
44
|
+
upsertMeta('property', 'og:image:alt', routeSeo.imageAlt);
|
|
45
|
+
upsertMeta('name', 'twitter:title', routeSeo.title);
|
|
46
|
+
upsertMeta('name', 'twitter:description', routeSeo.description);
|
|
47
|
+
upsertMeta('name', 'twitter:url', routeSeo.canonicalUrl);
|
|
48
|
+
upsertMeta('name', 'twitter:image', routeSeo.imageUrl);
|
|
49
|
+
upsertMeta('name', 'twitter:image:alt', routeSeo.imageAlt);
|
|
50
|
+
upsertLink('canonical', routeSeo.canonicalUrl);
|
|
51
|
+
}, [description, title, resolveRouteMeta]);
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|