@hashicorp/react-components 1.1.11 → 1.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/README.md +1 -1
- package/package.json +8 -9
- package/postcss.config.js +1 -1
- package/src/components/nav-panel/components/flight-icon/index.tsx +4 -2
- package/src/components/nav-panel/components/flight-icon/style.module.css +1 -1
- package/src/components/nav-panel/components/link/cloud-fill.svg +3 -0
- package/src/components/nav-panel/components/link/index.tsx +139 -0
- package/src/components/nav-panel/components/link/layers-fill.svg +5 -0
- package/src/components/nav-panel/components/link/shield-alert-fill.svg +3 -0
- package/src/components/nav-panel/components/link/style.module.css +64 -0
- package/src/components/nav-panel/components/link/zap-fill.svg +3 -0
- package/src/components/nav-panel/components/link-list/index.tsx +6 -64
- package/src/components/nav-panel/components/link-list/style.module.css +0 -49
- package/src/components/nav-panel/components/promo/index.tsx +23 -9
- package/src/components/nav-panel/components/promo/style.module.css +8 -18
- package/src/components/nav-panel/components/text/index.tsx +1 -1
- package/src/components/nav-panel/product-panel/index.tsx +38 -7
- package/src/components/nav-panel/product-panel/style.module.css +63 -46
- package/src/lib/get-localized-link-props/index.test.ts +1 -1
- package/src/lib/get-localized-link-props/index.ts +2 -2
- package/src/lib/parse-url/index.test.ts +19 -3
- package/src/lib/parse-url/index.ts +27 -9
- package/src/lib/send-posthog-event/index.ts +1 -1
- package/src/lib/supported-locales/index.ts +10 -1
- package/src/lib/track-nav-click-event/index.ts +1 -1
- package/src/lib/v/index.ts +8 -2
- package/tsconfig.json +1 -2
- package/.eslintrc.js +0 -4
- package/.stylelintrc.js +0 -3
package/README.md
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
# `@hashicorp/react-components`
|
|
2
2
|
|
|
3
|
-
This package contains React components that are built using HDS guidelines that don't align with existing HDS components.
|
|
3
|
+
This package contains React components that are built using HDS guidelines that don't align with existing HDS components.
|
package/package.json
CHANGED
|
@@ -1,26 +1,25 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hashicorp/react-components",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"main": "src/index.ts",
|
|
5
5
|
"scripts": {
|
|
6
|
-
"lint": "
|
|
6
|
+
"lint": "npm run lint:js",
|
|
7
|
+
"lint:js": "eslint . --quiet",
|
|
7
8
|
"test": "vitest run",
|
|
8
9
|
"test:watch": "vitest watch"
|
|
9
10
|
},
|
|
10
11
|
"dependencies": {
|
|
11
12
|
"@hashicorp/flight-icons": "^3.7.0",
|
|
12
|
-
"@
|
|
13
|
-
"@hashicorp/platform-util": "^0.2.0",
|
|
13
|
+
"@web/product-meta": "^0.1.1",
|
|
14
14
|
"@hashicorp/web-mktg-logos": "*",
|
|
15
|
-
"@radix-ui/react-navigation-menu": "^1.2.
|
|
16
|
-
"@radix-ui/react-visually-hidden": "^1.
|
|
15
|
+
"@radix-ui/react-navigation-menu": "^1.2.14",
|
|
16
|
+
"@radix-ui/react-visually-hidden": "^1.2.4",
|
|
17
17
|
"@vercel/fetch": "^7.0.0",
|
|
18
18
|
"@vercel/stega": "^0.1.2",
|
|
19
|
-
"
|
|
20
|
-
"
|
|
19
|
+
"classnames": "^2.3.2",
|
|
20
|
+
"posthog-js": "^1.195.0"
|
|
21
21
|
},
|
|
22
22
|
"devDependencies": {
|
|
23
|
-
"@hashicorp/platform-cli": "^2.8.0",
|
|
24
23
|
"@hashicorp/platform-types": "^0.4.0",
|
|
25
24
|
"@testing-library/jest-dom": "^6.4.2",
|
|
26
25
|
"@vitejs/plugin-react": "^4.2.1",
|
package/postcss.config.js
CHANGED
|
@@ -18,6 +18,7 @@ interface Props extends SVGProps<SVGSVGElement> {
|
|
|
18
18
|
color?: string
|
|
19
19
|
|
|
20
20
|
className?: string
|
|
21
|
+
size?: 16 | 24
|
|
21
22
|
}
|
|
22
23
|
|
|
23
24
|
// When an SVG file is imported in Next.js, it is an object with a `src`
|
|
@@ -30,6 +31,7 @@ export default function FlightIcon({
|
|
|
30
31
|
name,
|
|
31
32
|
color = 'currentColor',
|
|
32
33
|
className,
|
|
34
|
+
size = 16,
|
|
33
35
|
...rest
|
|
34
36
|
}: Props) {
|
|
35
37
|
const iconId = useId()
|
|
@@ -40,8 +42,8 @@ export default function FlightIcon({
|
|
|
40
42
|
aria-hidden="true"
|
|
41
43
|
fill={color}
|
|
42
44
|
id={iconId}
|
|
43
|
-
width={
|
|
44
|
-
height={
|
|
45
|
+
width={size}
|
|
46
|
+
height={size}
|
|
45
47
|
viewBox="0 0 16 16"
|
|
46
48
|
xmlns="http://www.w3.org/2000/svg"
|
|
47
49
|
>
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
+
<path fill-rule="evenodd" clip-rule="evenodd" d="M3.93179 2.37285C4.98771 1.98353 6.13197 1.89703 7.23466 2.12309C8.33737 2.34915 9.35467 2.87879 10.1708 3.65225C10.844 4.29015 11.358 5.07282 11.6757 5.93748H11.9554C13.0271 5.93748 14.0556 6.36147 14.8143 7.11713C15.5731 7.8729 16 8.89859 16 9.96872C16 11.0389 15.5731 12.0645 14.8143 12.8203C14.0556 13.576 13.0271 14 11.9554 14H6.03326C6.03078 14 6.02829 14 6.02579 14C4.90005 14.0008 3.79643 13.6872 2.84032 13.0944C1.88417 12.5016 1.11373 11.6533 0.616922 10.6456C0.120097 9.63781 -0.0830371 8.51125 0.0307977 7.39398C0.144632 6.27672 0.57082 5.21396 1.26058 4.32641C1.94849 3.44122 2.87099 2.76578 3.92346 2.37597C3.92623 2.37492 3.929 2.37388 3.93179 2.37285Z" fill="#EFEFF1"/>
|
|
3
|
+
</svg>
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import Image from 'next/image'
|
|
2
|
+
import Link from 'next/link'
|
|
3
|
+
import classNames from 'classnames'
|
|
4
|
+
import { vercelSplit } from '../../../../lib/v'
|
|
5
|
+
import { getLocalizedLinkProps } from '../../../../lib/get-localized-link-props'
|
|
6
|
+
import FlightIcon from '../flight-icon'
|
|
7
|
+
import * as NavigationMenu from '@radix-ui/react-navigation-menu'
|
|
8
|
+
import ShieldAlertFill from './shield-alert-fill.svg'
|
|
9
|
+
import CloudFill from './cloud-fill.svg'
|
|
10
|
+
import LayersFill from './layers-fill.svg'
|
|
11
|
+
import ZapFill from './zap-fill.svg'
|
|
12
|
+
import s from './style.module.css'
|
|
13
|
+
|
|
14
|
+
interface NavLinkProps {
|
|
15
|
+
title: string
|
|
16
|
+
url: string
|
|
17
|
+
icon?: string
|
|
18
|
+
description?: string
|
|
19
|
+
iconGradient?: string
|
|
20
|
+
linkClickHandler?: (title: string, url: string) => void
|
|
21
|
+
locale?: string
|
|
22
|
+
className?: string
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const GRADIENT_VALUE_MAP = new Map([
|
|
26
|
+
[
|
|
27
|
+
'ILM',
|
|
28
|
+
{
|
|
29
|
+
iconBackground: 'var(--mds-gradient-ilm)',
|
|
30
|
+
},
|
|
31
|
+
],
|
|
32
|
+
[
|
|
33
|
+
'SLM',
|
|
34
|
+
{
|
|
35
|
+
iconBackground: 'var(--mds-gradient-slm)',
|
|
36
|
+
},
|
|
37
|
+
],
|
|
38
|
+
[
|
|
39
|
+
'Cloud',
|
|
40
|
+
{
|
|
41
|
+
iconBackground: 'var(--mds-gradient-hashicorp)',
|
|
42
|
+
},
|
|
43
|
+
],
|
|
44
|
+
[
|
|
45
|
+
'Black',
|
|
46
|
+
{
|
|
47
|
+
iconBackground: 'var(--mds-color-surface-faint)',
|
|
48
|
+
},
|
|
49
|
+
],
|
|
50
|
+
[
|
|
51
|
+
'Solution Application',
|
|
52
|
+
{
|
|
53
|
+
iconBackground:
|
|
54
|
+
'linear-gradient(90deg, #a3f7d6 0%, #5cd1d9 50%, #5494e3 100%)',
|
|
55
|
+
},
|
|
56
|
+
],
|
|
57
|
+
['ILM Solid', { iconBackground: '#7948FF' }],
|
|
58
|
+
['SLM Solid', { iconBackground: '#FF6423' }],
|
|
59
|
+
])
|
|
60
|
+
|
|
61
|
+
export default function NavLink({
|
|
62
|
+
url,
|
|
63
|
+
title,
|
|
64
|
+
description,
|
|
65
|
+
icon,
|
|
66
|
+
iconGradient,
|
|
67
|
+
linkClickHandler,
|
|
68
|
+
locale,
|
|
69
|
+
className,
|
|
70
|
+
}: NavLinkProps) {
|
|
71
|
+
const style = iconGradient
|
|
72
|
+
? {
|
|
73
|
+
['--iconBackground' as string]: GRADIENT_VALUE_MAP.get(
|
|
74
|
+
vercelSplit(iconGradient),
|
|
75
|
+
)?.iconBackground,
|
|
76
|
+
}
|
|
77
|
+
: undefined
|
|
78
|
+
|
|
79
|
+
return (
|
|
80
|
+
<NavigationMenu.Link asChild>
|
|
81
|
+
<Link
|
|
82
|
+
className={classNames(s.link, className)}
|
|
83
|
+
{...getLocalizedLinkProps(url, locale)}
|
|
84
|
+
onClickCapture={() => {
|
|
85
|
+
linkClickHandler && linkClickHandler(title, url)
|
|
86
|
+
}}
|
|
87
|
+
>
|
|
88
|
+
{iconGradient && icon ? (
|
|
89
|
+
<div className={s.iconBackground} style={style} data-theme="dark">
|
|
90
|
+
<IconSwitcher icon={icon} iconGradient={iconGradient} />
|
|
91
|
+
</div>
|
|
92
|
+
) : (
|
|
93
|
+
icon && (
|
|
94
|
+
<div className={s.iconWrapper}>
|
|
95
|
+
<FlightIcon
|
|
96
|
+
name={vercelSplit(icon)}
|
|
97
|
+
color="var(--mds-color-foreground-faint)"
|
|
98
|
+
/>
|
|
99
|
+
</div>
|
|
100
|
+
)
|
|
101
|
+
)}
|
|
102
|
+
<div>
|
|
103
|
+
<div className={iconGradient && s.boldTitle}>{title}</div>
|
|
104
|
+
{description && <div className={s.description}>{description}</div>}
|
|
105
|
+
</div>
|
|
106
|
+
</Link>
|
|
107
|
+
</NavigationMenu.Link>
|
|
108
|
+
)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const IconSwitcher = ({
|
|
112
|
+
icon,
|
|
113
|
+
iconGradient,
|
|
114
|
+
}: {
|
|
115
|
+
icon: string
|
|
116
|
+
iconGradient: string
|
|
117
|
+
}) => {
|
|
118
|
+
switch (icon) {
|
|
119
|
+
case 'shield-alert':
|
|
120
|
+
return <Image src={ShieldAlertFill} alt="shield alert icon" />
|
|
121
|
+
case 'cloud':
|
|
122
|
+
return <Image src={CloudFill} alt="cloud icon" />
|
|
123
|
+
case 'layers':
|
|
124
|
+
return <Image src={LayersFill} alt="layers icon" />
|
|
125
|
+
case 'zap':
|
|
126
|
+
return <Image src={ZapFill} alt="zap icon" />
|
|
127
|
+
default:
|
|
128
|
+
return (
|
|
129
|
+
<FlightIcon
|
|
130
|
+
name={vercelSplit(icon)}
|
|
131
|
+
color={
|
|
132
|
+
iconGradient === 'ILM' || iconGradient === 'SLM'
|
|
133
|
+
? 'var(--mds-color-foreground-high-contrast)'
|
|
134
|
+
: 'var(--mds-color-foreground-strong)'
|
|
135
|
+
}
|
|
136
|
+
/>
|
|
137
|
+
)
|
|
138
|
+
}
|
|
139
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
+
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.65568 0.0838368C7.87179 -0.0279456 8.1287 -0.0279456 8.34481 0.0838368L15.5948 3.83384C15.8439 3.96265 16.0002 4.21962 16.0002 4.5C16.0002 4.78038 15.8439 5.03735 15.5948 5.16616L8.34481 8.91616C8.1287 9.02795 7.87179 9.02795 7.65568 8.91616L0.405677 5.16616C0.156637 5.03735 0.000244137 4.78038 0.000244137 4.5C0.000244137 4.21962 0.156637 3.96265 0.405677 3.83384L7.65568 0.0838368Z" fill="#EFEFF1"/>
|
|
3
|
+
<path d="M0.0934554 10.8876C0.293553 10.5249 0.749771 10.3931 1.11245 10.5932L8.00014 14.3933L14.8878 10.5932C15.2505 10.3931 15.7067 10.5249 15.9068 10.8876C16.1069 11.2503 15.9751 11.7065 15.6124 11.9066L8.36245 15.9066C8.13693 16.031 7.86335 16.031 7.63783 15.9066L0.387831 11.9066C0.0251545 11.7065 -0.106642 11.2503 0.0934554 10.8876Z" fill="#EFEFF1"/>
|
|
4
|
+
<path d="M1.11245 7.09321C0.749771 6.89311 0.293553 7.02491 0.0934554 7.38759C-0.106642 7.75026 0.0251545 8.20648 0.387831 8.40658L7.63783 12.4066C7.86335 12.531 8.13693 12.531 8.36245 12.4066L15.6124 8.40658C15.9751 8.20648 16.1069 7.75026 15.9068 7.38759C15.7067 7.02491 15.2505 6.89311 14.8878 7.09321L8.00014 10.8933L1.11245 7.09321Z" fill="#EFEFF1"/>
|
|
5
|
+
</svg>
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
<svg width="14" height="16" viewBox="0 0 14 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
+
<path d="M6.26074 0.125C6.73954 -0.0415734 7.26046 -0.0415732 7.73926 0.125L12.4893 1.77832C13.3937 2.09305 14 2.94572 14 3.90332V7.91211C13.9998 9.96031 12.8191 11.6595 11.5498 12.9033C10.2686 14.1588 8.79248 15.0539 7.97656 15.501C7.36496 15.8361 6.63504 15.8361 6.02344 15.501C5.20752 15.0539 3.7314 14.1588 2.4502 12.9033C1.18094 11.6595 0.00016175 9.96031 0 7.91211V3.90332C0 2.94572 0.606344 2.09305 1.51074 1.77832L6.26074 0.125ZM7 10C6.44772 10 6 10.4477 6 11C6.00004 11.5522 6.44774 12 7 12H7.00684C7.55902 11.9999 8.00679 11.5522 8.00684 11C8.00684 10.4478 7.55904 10.0001 7.00684 10H7ZM7 4C6.58579 4 6.25 4.33579 6.25 4.75V8.25C6.25004 8.66418 6.58581 9 7 9C7.41419 9 7.74996 8.66418 7.75 8.25C7.75 8.25 7.75 5.16452 7.75 4.75C7.75 4.33579 7.41421 4 7 4Z" fill="white"/>
|
|
3
|
+
</svg>
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
.linkList {
|
|
2
|
+
display: flex;
|
|
3
|
+
list-style: none;
|
|
4
|
+
flex-direction: column;
|
|
5
|
+
gap: var(--mds-spacing-03);
|
|
6
|
+
margin: 0;
|
|
7
|
+
padding: 0;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
.link {
|
|
11
|
+
color: var(--mds-color-foreground-primary);
|
|
12
|
+
display: flex;
|
|
13
|
+
gap: var(--mds-spacing-04);
|
|
14
|
+
padding: var(--mds-spacing-03) var(--mds-spacing-05);
|
|
15
|
+
min-width: 190px;
|
|
16
|
+
align-items: flex-start;
|
|
17
|
+
height: 100%;
|
|
18
|
+
|
|
19
|
+
&:hover {
|
|
20
|
+
background-color: var(--mds-color-surface-interactive-hover);
|
|
21
|
+
border-radius: var(--mds-spacing-01);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
&:has(.iconBackground) {
|
|
25
|
+
align-items: flex-start;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
@media (--large) {
|
|
29
|
+
max-width: 416px;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
&:focus {
|
|
33
|
+
outline: 2px solid var(--mds-color-focus-action-internal);
|
|
34
|
+
outline-style: auto;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.description {
|
|
39
|
+
font-size: var(--mds-typography-body-200-font-size);
|
|
40
|
+
font-weight: var(--mds-typography-font-weight-regular);
|
|
41
|
+
margin-top: var(--mds-spacing-02);
|
|
42
|
+
color: var(--mds-color-foreground-faint);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.iconBackground {
|
|
46
|
+
background: var(--iconBackground);
|
|
47
|
+
height: var(--mds-spacing-08);
|
|
48
|
+
width: var(--mds-spacing-08);
|
|
49
|
+
display: flex;
|
|
50
|
+
align-items: center;
|
|
51
|
+
justify-content: center;
|
|
52
|
+
border-radius: 3px;
|
|
53
|
+
flex: none;
|
|
54
|
+
margin-top: 3px;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.boldTitle {
|
|
58
|
+
font-weight: var(--mds-typography-font-weight-semibold);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.iconWrapper {
|
|
62
|
+
margin-top: 3px;
|
|
63
|
+
height: fit-content;
|
|
64
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
+
<path fill-rule="evenodd" clip-rule="evenodd" d="M9.3747 0.078024C9.67891 0.228777 9.8439 0.565087 9.77695 0.897934L8.88834 5.31559L14.3726 6.22441C14.6414 6.26896 14.865 6.45581 14.9565 6.71247C15.048 6.96914 14.9931 7.25524 14.813 7.4598L7.52137 15.7455C7.29708 16.0004 6.92954 16.0728 6.62533 15.922C6.32112 15.7713 6.15612 15.435 6.22307 15.1021L7.11168 10.6845L1.6274 9.77566C1.35858 9.73111 1.13506 9.54426 1.04356 9.2876C0.952061 9.03093 1.00697 8.74483 1.18699 8.54027L8.47865 0.254555C8.70295 -0.000319332 9.07049 -0.0727287 9.3747 0.078024Z" fill="#EFEFF1"/>
|
|
3
|
+
</svg>
|
|
@@ -1,8 +1,4 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { vercelSplit } from '../../../../lib/v'
|
|
3
|
-
import { getLocalizedLinkProps } from '../../../../lib/get-localized-link-props'
|
|
4
|
-
import FlightIcon from '../flight-icon'
|
|
5
|
-
import * as NavigationMenu from '@radix-ui/react-navigation-menu'
|
|
1
|
+
import NavLink from '../link'
|
|
6
2
|
import s from './style.module.css'
|
|
7
3
|
|
|
8
4
|
export interface LinkListProps {
|
|
@@ -17,27 +13,6 @@ export interface LinkListProps {
|
|
|
17
13
|
linkClickHandler?: (title: string, url: string) => void
|
|
18
14
|
}
|
|
19
15
|
|
|
20
|
-
const GRADIENT_VALUE_MAP = new Map([
|
|
21
|
-
[
|
|
22
|
-
'ILM',
|
|
23
|
-
{
|
|
24
|
-
iconGradient: 'var(--icon-gradient-infrastructure-lifecycle-management)',
|
|
25
|
-
},
|
|
26
|
-
],
|
|
27
|
-
[
|
|
28
|
-
'SLM',
|
|
29
|
-
{
|
|
30
|
-
iconGradient: 'var(--icon-gradient-security-lifecycle-management)',
|
|
31
|
-
},
|
|
32
|
-
],
|
|
33
|
-
[
|
|
34
|
-
'Cloud',
|
|
35
|
-
{
|
|
36
|
-
iconGradient: 'var(--icon-gradient-infrastructure-cloud)',
|
|
37
|
-
},
|
|
38
|
-
],
|
|
39
|
-
])
|
|
40
|
-
|
|
41
16
|
export default function LinkList({
|
|
42
17
|
locale,
|
|
43
18
|
links,
|
|
@@ -46,46 +21,13 @@ export default function LinkList({
|
|
|
46
21
|
return (
|
|
47
22
|
<ul className={s.linkList}>
|
|
48
23
|
{links.map((link) => {
|
|
49
|
-
let style
|
|
50
|
-
link.iconGradient
|
|
51
|
-
? (style = {
|
|
52
|
-
'--iconGradient': GRADIENT_VALUE_MAP.get(
|
|
53
|
-
vercelSplit(link.iconGradient)
|
|
54
|
-
)?.iconGradient,
|
|
55
|
-
} as React.CSSProperties)
|
|
56
|
-
: (style = null)
|
|
57
24
|
return (
|
|
58
25
|
<li key={link.title}>
|
|
59
|
-
<
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
linkClickHandler && linkClickHandler(link.title, link.url)
|
|
65
|
-
}}
|
|
66
|
-
>
|
|
67
|
-
{link.iconGradient && link.icon ? (
|
|
68
|
-
<div className={s.iconBackground} style={style}>
|
|
69
|
-
<FlightIcon name={vercelSplit(link.icon)} />
|
|
70
|
-
</div>
|
|
71
|
-
) : (
|
|
72
|
-
link.icon && (
|
|
73
|
-
<FlightIcon
|
|
74
|
-
name={vercelSplit(link.icon)}
|
|
75
|
-
color="var(--token-color-foreground-faint)"
|
|
76
|
-
/>
|
|
77
|
-
)
|
|
78
|
-
)}
|
|
79
|
-
<div>
|
|
80
|
-
<div className={link.iconGradient && s.boldTitle}>
|
|
81
|
-
{link.title}
|
|
82
|
-
</div>
|
|
83
|
-
{link.description && (
|
|
84
|
-
<div className={s.description}>{link.description}</div>
|
|
85
|
-
)}
|
|
86
|
-
</div>
|
|
87
|
-
</Link>
|
|
88
|
-
</NavigationMenu.Link>
|
|
26
|
+
<NavLink
|
|
27
|
+
{...link}
|
|
28
|
+
linkClickHandler={linkClickHandler}
|
|
29
|
+
locale={locale}
|
|
30
|
+
/>
|
|
89
31
|
</li>
|
|
90
32
|
)
|
|
91
33
|
})}
|
|
@@ -6,52 +6,3 @@
|
|
|
6
6
|
margin: 0;
|
|
7
7
|
padding: 0;
|
|
8
8
|
}
|
|
9
|
-
|
|
10
|
-
.link {
|
|
11
|
-
color: var(--token-color-foreground-primary);
|
|
12
|
-
display: flex;
|
|
13
|
-
gap: var(--mds-spacing-04);
|
|
14
|
-
padding: var(--mds-spacing-03) var(--mds-spacing-04);
|
|
15
|
-
min-width: 190px;
|
|
16
|
-
align-items: center;
|
|
17
|
-
|
|
18
|
-
&:hover {
|
|
19
|
-
background-color: var(--token-color-surface-interactive-hover);
|
|
20
|
-
border-radius: var(--mds-spacing-01);
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
&:has(.iconBackground) {
|
|
24
|
-
align-items: flex-start;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
@media (--large) {
|
|
28
|
-
max-width: 416px;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
&:focus {
|
|
32
|
-
outline: 2px solid var(--token-color-focus-action-internal);
|
|
33
|
-
outline-style: auto;
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
.description {
|
|
38
|
-
font-size: var(--mds-typography-body-200-font-size);
|
|
39
|
-
font-weight: var(--mds-typography-font-weight-regular);
|
|
40
|
-
margin-top: var(--mds-spacing-02);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
.iconBackground {
|
|
44
|
-
background: var(--iconGradient);
|
|
45
|
-
height: var(--mds-spacing-08);
|
|
46
|
-
width: var(--mds-spacing-08);
|
|
47
|
-
display: flex;
|
|
48
|
-
align-items: center;
|
|
49
|
-
justify-content: center;
|
|
50
|
-
border-radius: 3px;
|
|
51
|
-
flex: none;
|
|
52
|
-
margin-top: 3px;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
.boldTitle {
|
|
56
|
-
font-weight: var(--mds-typography-font-weight-semibold);
|
|
57
|
-
}
|
|
@@ -11,6 +11,7 @@ import s from './style.module.css'
|
|
|
11
11
|
export interface PromoProps {
|
|
12
12
|
locale?: string
|
|
13
13
|
title: string
|
|
14
|
+
titleLinkUrl?: string
|
|
14
15
|
description: string
|
|
15
16
|
linkUrl: string
|
|
16
17
|
linkTitle: string
|
|
@@ -24,6 +25,7 @@ export interface PromoProps {
|
|
|
24
25
|
export default function Promo({
|
|
25
26
|
locale,
|
|
26
27
|
title,
|
|
28
|
+
titleLinkUrl,
|
|
27
29
|
description,
|
|
28
30
|
linkUrl,
|
|
29
31
|
linkTitle,
|
|
@@ -60,19 +62,31 @@ export default function Promo({
|
|
|
60
62
|
[s.hashiconf]: vercelSplit(theme) === 'hashiconf',
|
|
61
63
|
})}
|
|
62
64
|
>
|
|
63
|
-
<FlightIcon name={vercelSplit(icon)} />
|
|
65
|
+
<FlightIcon name={vercelSplit(icon)} size={24} />
|
|
64
66
|
</div>
|
|
65
67
|
)}
|
|
66
68
|
<div className={s.rightContent}>
|
|
67
69
|
<div>
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
70
|
+
{titleLinkUrl ? (
|
|
71
|
+
<a
|
|
72
|
+
className={classNames(s.title, s.link, {
|
|
73
|
+
[s.hashidays]: vercelSplit(theme) === 'hashidays',
|
|
74
|
+
[s.hashiconf]: vercelSplit(theme) === 'hashiconf',
|
|
75
|
+
})}
|
|
76
|
+
href={titleLinkUrl}
|
|
77
|
+
>
|
|
78
|
+
{title}
|
|
79
|
+
</a>
|
|
80
|
+
) : (
|
|
81
|
+
<div
|
|
82
|
+
className={classNames(s.title, {
|
|
83
|
+
[s.hashidays]: vercelSplit(theme) === 'hashidays',
|
|
84
|
+
[s.hashiconf]: vercelSplit(theme) === 'hashiconf',
|
|
85
|
+
})}
|
|
86
|
+
>
|
|
87
|
+
{title}
|
|
88
|
+
</div>
|
|
89
|
+
)}
|
|
76
90
|
<div className={s.description}>{description}</div>
|
|
77
91
|
</div>
|
|
78
92
|
<NavigationMenu.Link asChild>
|
|
@@ -3,10 +3,9 @@
|
|
|
3
3
|
.promo {
|
|
4
4
|
display: flex;
|
|
5
5
|
border-radius: var(--mds-spacing-01);
|
|
6
|
-
border: 1px solid var(--
|
|
7
|
-
background-color: var(--
|
|
8
|
-
padding: var(--mds-spacing-05) var(--mds-spacing-08) var(--mds-spacing-06)
|
|
9
|
-
var(--mds-spacing-08);
|
|
6
|
+
border: 1px solid var(--mds-color-border-faint);
|
|
7
|
+
background-color: var(--mds-color-surface-faint);
|
|
8
|
+
padding: var(--mds-spacing-05) var(--mds-spacing-08) var(--mds-spacing-06) var(--mds-spacing-08);
|
|
10
9
|
flex-direction: row;
|
|
11
10
|
gap: var(--mds-spacing-04);
|
|
12
11
|
margin: var(--mds-spacing-07);
|
|
@@ -80,7 +79,7 @@
|
|
|
80
79
|
}
|
|
81
80
|
|
|
82
81
|
.link {
|
|
83
|
-
color: var(--
|
|
82
|
+
color: var(--mds-color-foreground-strong);
|
|
84
83
|
display: flex;
|
|
85
84
|
flex-direction: row;
|
|
86
85
|
align-items: center;
|
|
@@ -91,18 +90,13 @@
|
|
|
91
90
|
}
|
|
92
91
|
|
|
93
92
|
&:focus {
|
|
94
|
-
outline: 2px solid var(--
|
|
93
|
+
outline: 2px solid var(--mds-color-focus-action-internal);
|
|
95
94
|
outline-style: auto;
|
|
96
95
|
}
|
|
97
96
|
}
|
|
98
97
|
|
|
99
98
|
.hashidays {
|
|
100
|
-
background: linear-gradient(
|
|
101
|
-
90deg,
|
|
102
|
-
#7b54b6 0%,
|
|
103
|
-
#4c69d8 74.77%,
|
|
104
|
-
#60d0dc 115.93%
|
|
105
|
-
);
|
|
99
|
+
background: linear-gradient(90deg, #7b54b6 0%, #4c69d8 74.77%, #60d0dc 115.93%);
|
|
106
100
|
-webkit-background-clip: text;
|
|
107
101
|
-webkit-text-fill-color: transparent;
|
|
108
102
|
max-width: fit-content;
|
|
@@ -113,16 +107,12 @@
|
|
|
113
107
|
}
|
|
114
108
|
|
|
115
109
|
.hashiconf {
|
|
116
|
-
background: linear-gradient(
|
|
117
|
-
to right,
|
|
118
|
-
var(--token-color-terraform-foreground),
|
|
119
|
-
var(--token-color-vagrant-foreground)
|
|
120
|
-
);
|
|
110
|
+
background: linear-gradient(to right, var(--mds-color-terraform-foreground), var(--mds-color-vagrant-foreground));
|
|
121
111
|
-webkit-background-clip: text;
|
|
122
112
|
-webkit-text-fill-color: transparent;
|
|
123
113
|
max-width: fit-content;
|
|
124
114
|
|
|
125
115
|
& svg {
|
|
126
|
-
color: var(--
|
|
116
|
+
color: var(--mds-color-terraform-foreground);
|
|
127
117
|
}
|
|
128
118
|
}
|
|
@@ -3,17 +3,20 @@ import Link from 'next/link'
|
|
|
3
3
|
import * as NavPanel from '../components/panel'
|
|
4
4
|
import Promo, { type PromoProps } from '../components/promo'
|
|
5
5
|
import { getLocalizedLinkProps } from '../../../lib/get-localized-link-props'
|
|
6
|
-
import { Products } from '@
|
|
6
|
+
import { Products } from '@web/product-meta'
|
|
7
7
|
import FlightIcon from '../components/flight-icon'
|
|
8
8
|
import { Text } from '../components/text'
|
|
9
9
|
import { trackNavClickEvent } from '../../../lib/track-nav-click-event'
|
|
10
10
|
import * as NavigationMenu from '@radix-ui/react-navigation-menu'
|
|
11
|
+
import { LinkListProps } from '../components/link-list'
|
|
12
|
+
import NavLink from '../components/link'
|
|
11
13
|
import s from './style.module.css'
|
|
12
14
|
|
|
13
15
|
const PRODUCT_LOGO_MAP = new Map([
|
|
14
16
|
['Terraform', 'terraform-fill'],
|
|
15
17
|
['Packer', 'packer-fill-color'],
|
|
16
18
|
['Vault', 'vault-fill-color'],
|
|
19
|
+
['Vault Radar', 'vault-fill-color'],
|
|
17
20
|
['Boundary', 'boundary-fill-color'],
|
|
18
21
|
['Consul', 'consul-fill-color'],
|
|
19
22
|
['Nomad', 'nomad-fill-color'],
|
|
@@ -29,9 +32,15 @@ const PRODUCT_LOGO_EXCEPTION_CLASSNAMES = {
|
|
|
29
32
|
'HCP Vault Radar': 'hcpVaultRadar',
|
|
30
33
|
}
|
|
31
34
|
|
|
35
|
+
export type ProductCategoriesProps = Array<{
|
|
36
|
+
title: string
|
|
37
|
+
group?: LinkListProps['links'][0]
|
|
38
|
+
products: NavProductProps[]
|
|
39
|
+
}>
|
|
40
|
+
|
|
32
41
|
export interface ProductsPanelProps {
|
|
33
42
|
locale?: string
|
|
34
|
-
productCategories:
|
|
43
|
+
productCategories: ProductCategoriesProps
|
|
35
44
|
promo?: PromoProps
|
|
36
45
|
sidePanel?: {
|
|
37
46
|
label: string
|
|
@@ -71,10 +80,19 @@ const ProductPanel = ({
|
|
|
71
80
|
) : null}
|
|
72
81
|
<NavPanel.Column hasSidePanel={Boolean(sidePanel)}>
|
|
73
82
|
<ul className={s.productCategories}>
|
|
74
|
-
{productCategories.map(({ title, products }) => {
|
|
83
|
+
{productCategories.map(({ group, title, products }) => {
|
|
75
84
|
return (
|
|
76
|
-
<li
|
|
77
|
-
|
|
85
|
+
<li
|
|
86
|
+
key={title}
|
|
87
|
+
className={classNames(s.productCategory, {
|
|
88
|
+
[s.productCategoryHasGroup]: group,
|
|
89
|
+
})}
|
|
90
|
+
>
|
|
91
|
+
{group ? (
|
|
92
|
+
<NavLink {...group} className={s.productGroupLink} />
|
|
93
|
+
) : (
|
|
94
|
+
<Text className={s.eyebrow}>{title}</Text>
|
|
95
|
+
)}
|
|
78
96
|
<ul className={s.productList}>
|
|
79
97
|
{products.map((product) => {
|
|
80
98
|
return (
|
|
@@ -118,7 +136,7 @@ const ProductPanel = ({
|
|
|
118
136
|
<div className={s.iconBackground}>
|
|
119
137
|
<FlightIcon
|
|
120
138
|
name={icon}
|
|
121
|
-
color="var(--
|
|
139
|
+
color="var(--mds-color-foreground-faint)"
|
|
122
140
|
/>
|
|
123
141
|
</div>
|
|
124
142
|
<div className={s.sidePanelItemContent}>
|
|
@@ -173,7 +191,20 @@ function NavProduct({ locale, url, product, description }: NavProductProps) {
|
|
|
173
191
|
/>
|
|
174
192
|
</div>
|
|
175
193
|
<div className={s.productTextContent}>
|
|
176
|
-
|
|
194
|
+
{product === 'Vault Radar' ? (
|
|
195
|
+
<div>
|
|
196
|
+
<Text size="200" weight="semibold">
|
|
197
|
+
Vault
|
|
198
|
+
</Text>
|
|
199
|
+
<Text size="200" weight="regular">
|
|
200
|
+
Radar
|
|
201
|
+
</Text>
|
|
202
|
+
</div>
|
|
203
|
+
) : (
|
|
204
|
+
<Text size="200" weight="semibold">
|
|
205
|
+
{product}
|
|
206
|
+
</Text>
|
|
207
|
+
)}
|
|
177
208
|
<span className={s.productDesc}>{description}</span>
|
|
178
209
|
</div>
|
|
179
210
|
</div>
|
|
@@ -3,29 +3,6 @@
|
|
|
3
3
|
.productPanel {
|
|
4
4
|
height: 100%;
|
|
5
5
|
|
|
6
|
-
/* Temporarily included for devdot compatability
|
|
7
|
-
https://app.asana.com/1/90955849329269/project/1209320521286316/task/1210514680913728?focus=true */
|
|
8
|
-
--mds-spacing-01: 0.25rem;
|
|
9
|
-
--mds-spacing-02: 0.375rem;
|
|
10
|
-
--mds-spacing-03: 0.5rem;
|
|
11
|
-
--mds-spacing-04: 0.75rem;
|
|
12
|
-
--mds-spacing-05: 1rem;
|
|
13
|
-
--mds-spacing-06: 1.25rem;
|
|
14
|
-
--mds-spacing-07: 1.5rem;
|
|
15
|
-
--mds-spacing-08: 2rem;
|
|
16
|
-
--mds-spacing-09: 2.5rem;
|
|
17
|
-
--mds-spacing-10: 3rem;
|
|
18
|
-
--mds-spacing-11: 3.5rem;
|
|
19
|
-
--mds-spacing-12: 4rem;
|
|
20
|
-
--mds-spacing-13: 4.5rem;
|
|
21
|
-
--mds-spacing-14: 5.5rem;
|
|
22
|
-
--mds-spacing-15: 6rem;
|
|
23
|
-
--mds-spacing-16: 8rem;
|
|
24
|
-
--mds-spacing-17: 8.5rem;
|
|
25
|
-
--mds-typography-body-200-font-size: 0.875rem;
|
|
26
|
-
--mds-typography-font-weight-regular: 400;
|
|
27
|
-
--mds-typography-font-weight-semibold: 600;
|
|
28
|
-
|
|
29
6
|
@media (--large) {
|
|
30
7
|
&.noSide {
|
|
31
8
|
display: grid;
|
|
@@ -74,7 +51,7 @@
|
|
|
74
51
|
min-width: 160px;
|
|
75
52
|
|
|
76
53
|
&:first-child {
|
|
77
|
-
border-bottom: 1px solid var(--
|
|
54
|
+
border-bottom: 1px solid var(--mds-color-border-primary);
|
|
78
55
|
margin-bottom: var(--mds-spacing-05);
|
|
79
56
|
padding-bottom: var(--mds-spacing-05);
|
|
80
57
|
}
|
|
@@ -82,7 +59,7 @@
|
|
|
82
59
|
|
|
83
60
|
.eyebrow {
|
|
84
61
|
padding-bottom: var(--mds-spacing-04);
|
|
85
|
-
color: var(--
|
|
62
|
+
color: var(--mds-color-foreground-strong);
|
|
86
63
|
font-weight: var(--mds-typography-font-weight-semibold);
|
|
87
64
|
|
|
88
65
|
@media (--large) {
|
|
@@ -147,7 +124,7 @@
|
|
|
147
124
|
margin-bottom: 0;
|
|
148
125
|
|
|
149
126
|
&:hover {
|
|
150
|
-
background-color: var(--
|
|
127
|
+
background-color: var(--mds-color-surface-interactive-hover);
|
|
151
128
|
border-radius: var(--mds-spacing-01);
|
|
152
129
|
}
|
|
153
130
|
}
|
|
@@ -159,7 +136,7 @@
|
|
|
159
136
|
margin-bottom: 0;
|
|
160
137
|
|
|
161
138
|
&:hover {
|
|
162
|
-
background-color: var(--
|
|
139
|
+
background-color: var(--mds-color-surface-interactive-hover);
|
|
163
140
|
border-radius: var(--mds-spacing-01);
|
|
164
141
|
}
|
|
165
142
|
}
|
|
@@ -196,7 +173,7 @@
|
|
|
196
173
|
.productTextContent {
|
|
197
174
|
display: flex;
|
|
198
175
|
flex-direction: column;
|
|
199
|
-
color: var(--
|
|
176
|
+
color: var(--mds-color-foreground-strong);
|
|
200
177
|
}
|
|
201
178
|
|
|
202
179
|
.productDesc {
|
|
@@ -204,12 +181,12 @@
|
|
|
204
181
|
font-size: 14px;
|
|
205
182
|
line-height: 20px;
|
|
206
183
|
letter-spacing: 0.0125em;
|
|
207
|
-
color: var(--
|
|
184
|
+
color: var(--mds-color-foreground-faint);
|
|
208
185
|
}
|
|
209
186
|
|
|
210
187
|
.focusIndicator {
|
|
211
188
|
&:focus {
|
|
212
|
-
outline: 2px solid var(--
|
|
189
|
+
outline: 2px solid var(--mds-color-focus-action-internal);
|
|
213
190
|
outline-style: auto;
|
|
214
191
|
}
|
|
215
192
|
}
|
|
@@ -226,8 +203,7 @@
|
|
|
226
203
|
|
|
227
204
|
@media (--large) {
|
|
228
205
|
@nest .noSide & {
|
|
229
|
-
padding: var(--mds-spacing-
|
|
230
|
-
var(--mds-spacing-04);
|
|
206
|
+
padding: var(--mds-spacing-05) 0 var(--mds-spacing-05) var(--mds-spacing-04);
|
|
231
207
|
}
|
|
232
208
|
|
|
233
209
|
@nest .topPromo & {
|
|
@@ -237,8 +213,7 @@
|
|
|
237
213
|
|
|
238
214
|
@media (--dev-dot-viewport) {
|
|
239
215
|
@nest .hasSide & {
|
|
240
|
-
padding: var(--mds-spacing-07) 0 var(--mds-spacing-09)
|
|
241
|
-
var(--mds-spacing-04);
|
|
216
|
+
padding: var(--mds-spacing-07) 0 var(--mds-spacing-09) var(--mds-spacing-04);
|
|
242
217
|
}
|
|
243
218
|
|
|
244
219
|
@nest .topPromo & {
|
|
@@ -249,13 +224,12 @@
|
|
|
249
224
|
|
|
250
225
|
.sidePanel {
|
|
251
226
|
height: 100%;
|
|
252
|
-
background: var(--
|
|
227
|
+
background: var(--mds-color-surface-faint);
|
|
253
228
|
display: flex;
|
|
254
229
|
flex-direction: column;
|
|
255
230
|
gap: var(--mds-spacing-05);
|
|
256
231
|
margin: 0;
|
|
257
|
-
padding: var(--mds-spacing-07) var(--mds-spacing-04) var(--mds-spacing-09)
|
|
258
|
-
var(--mds-spacing-07);
|
|
232
|
+
padding: var(--mds-spacing-07) var(--mds-spacing-04) var(--mds-spacing-09) var(--mds-spacing-07);
|
|
259
233
|
}
|
|
260
234
|
|
|
261
235
|
.sidePanelItems {
|
|
@@ -280,7 +254,7 @@
|
|
|
280
254
|
padding: var(--mds-spacing-03) var(--mds-spacing-05);
|
|
281
255
|
|
|
282
256
|
&:hover {
|
|
283
|
-
background-color: var(--
|
|
257
|
+
background-color: var(--mds-color-surface-interactive-hover);
|
|
284
258
|
border-radius: var(--mds-spacing-01);
|
|
285
259
|
}
|
|
286
260
|
}
|
|
@@ -289,7 +263,7 @@
|
|
|
289
263
|
/* Lifted from:
|
|
290
264
|
packages/react-components/src/components/nav-panel/components/link-list/style.module.css */
|
|
291
265
|
.iconBackground {
|
|
292
|
-
background: var(--
|
|
266
|
+
background: var(--mds-color-foreground-high-contrast);
|
|
293
267
|
height: var(--mds-spacing-08);
|
|
294
268
|
width: var(--mds-spacing-08);
|
|
295
269
|
display: flex;
|
|
@@ -298,21 +272,21 @@ packages/react-components/src/components/nav-panel/components/link-list/style.mo
|
|
|
298
272
|
border-radius: 3px;
|
|
299
273
|
flex: none;
|
|
300
274
|
margin-top: 3px;
|
|
301
|
-
border: 1px solid var(--
|
|
275
|
+
border: 1px solid var(--mds-color-border-primary);
|
|
302
276
|
}
|
|
303
277
|
|
|
304
278
|
/* Lifted from .productTextContent */
|
|
305
279
|
.sidePanelItemContent {
|
|
306
280
|
display: flex;
|
|
307
281
|
flex-direction: column;
|
|
308
|
-
color: var(--
|
|
282
|
+
color: var(--mds-color-foreground-strong);
|
|
309
283
|
}
|
|
310
284
|
|
|
311
285
|
.sidePanelItemDesc {
|
|
312
286
|
font-weight: 400;
|
|
313
287
|
font-size: 13px;
|
|
314
288
|
line-height: 18px;
|
|
315
|
-
color: var(--
|
|
289
|
+
color: var(--mds-color-foreground-primary);
|
|
316
290
|
}
|
|
317
291
|
|
|
318
292
|
/* Branding exception classnames */
|
|
@@ -320,10 +294,10 @@ packages/react-components/src/components/nav-panel/components/link-list/style.mo
|
|
|
320
294
|
/* Allows switching to a dark-mode-specific color for Terraform */
|
|
321
295
|
.terraform {
|
|
322
296
|
& use {
|
|
323
|
-
fill: var(--
|
|
297
|
+
fill: var(--mds-color-terraform-brand);
|
|
324
298
|
|
|
325
299
|
@nest [data-theme='dark'] & {
|
|
326
|
-
fill: var(--
|
|
300
|
+
fill: var(--mds-color-terraform-foreground);
|
|
327
301
|
}
|
|
328
302
|
}
|
|
329
303
|
|
|
@@ -339,11 +313,11 @@ packages/react-components/src/components/nav-panel/components/link-list/style.mo
|
|
|
339
313
|
.hcpVaultSecrets,
|
|
340
314
|
.hcpVaultRadar {
|
|
341
315
|
padding: var(--mds-spacing-01);
|
|
342
|
-
border: 2px solid var(--
|
|
316
|
+
border: 2px solid var(--mds-color-vault-brand);
|
|
343
317
|
border-radius: 3px;
|
|
344
318
|
|
|
345
319
|
& use {
|
|
346
|
-
fill: var(--
|
|
320
|
+
fill: var(--mds-color-foreground-strong);
|
|
347
321
|
}
|
|
348
322
|
|
|
349
323
|
& path {
|
|
@@ -354,3 +328,46 @@ packages/react-components/src/components/nav-panel/components/link-list/style.mo
|
|
|
354
328
|
.sidePanelText {
|
|
355
329
|
font-weight: var(--mds-typography-font-weight-semibold);
|
|
356
330
|
}
|
|
331
|
+
|
|
332
|
+
.productCategoryHasGroup {
|
|
333
|
+
& .productList {
|
|
334
|
+
padding-left: 0;
|
|
335
|
+
padding-right: 0;
|
|
336
|
+
padding-top: 0;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
& .productWrapper {
|
|
340
|
+
gap: var(--mds-spacing-04);
|
|
341
|
+
|
|
342
|
+
@media (--small) {
|
|
343
|
+
padding: var(--mds-spacing-03) var(--mds-spacing-05);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
& .productLogo {
|
|
348
|
+
margin-top: 3px;
|
|
349
|
+
height: 32px;
|
|
350
|
+
width: 32px;
|
|
351
|
+
min-width: 32px;
|
|
352
|
+
|
|
353
|
+
& svg {
|
|
354
|
+
width: 100%;
|
|
355
|
+
height: 100%;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
& .terraform {
|
|
360
|
+
width: 100%;
|
|
361
|
+
height: 100%;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
.productGroupLink {
|
|
366
|
+
min-width: 100%;
|
|
367
|
+
max-width: 100%;
|
|
368
|
+
margin-bottom: var(--mds-spacing-03);
|
|
369
|
+
color: var(--mds-color-foreground-strong);
|
|
370
|
+
font-family: var(--mds-typography-font-stack-text);
|
|
371
|
+
font-size: var(--mds-typography-body-200-font-size);
|
|
372
|
+
line-height: var(--mds-typography-body-200-line-height);
|
|
373
|
+
}
|
|
@@ -16,7 +16,7 @@ describe('getLocalizedLinkProps', () => {
|
|
|
16
16
|
it('does format paths that start with a locale string', () => {
|
|
17
17
|
const localizedLinkProps = getLocalizedLinkProps(
|
|
18
18
|
'/deconstructing-cloud',
|
|
19
|
-
'en'
|
|
19
|
+
'en',
|
|
20
20
|
)
|
|
21
21
|
expect(localizedLinkProps.href).toBe('/en/deconstructing-cloud')
|
|
22
22
|
expect(localizedLinkProps.hrefLang).toBe('en')
|
|
@@ -3,7 +3,7 @@ import { allSupportedLocales } from '../supported-locales'
|
|
|
3
3
|
|
|
4
4
|
export const getLocalizedLinkProps = (
|
|
5
5
|
url: string,
|
|
6
|
-
locale?: string
|
|
6
|
+
locale?: string,
|
|
7
7
|
): {
|
|
8
8
|
href: string
|
|
9
9
|
hrefLang: string | undefined
|
|
@@ -12,7 +12,7 @@ export const getLocalizedLinkProps = (
|
|
|
12
12
|
|
|
13
13
|
// example: /en
|
|
14
14
|
const hrefIsLocalizedHomepage = allSupportedLocales.includes(
|
|
15
|
-
parsedHref.substring(1)
|
|
15
|
+
parsedHref.substring(1),
|
|
16
16
|
)
|
|
17
17
|
|
|
18
18
|
// true example: /en/solutions
|
|
@@ -29,7 +29,7 @@ describe('parseUrl', () => {
|
|
|
29
29
|
|
|
30
30
|
it('leaves URL search params intact', () => {
|
|
31
31
|
const url = parseUrl(
|
|
32
|
-
'https://hashicorp.com/resources?type=case-study&products=terraform'
|
|
32
|
+
'https://hashicorp.com/resources?type=case-study&products=terraform',
|
|
33
33
|
)
|
|
34
34
|
expect(url.href).toBe('/resources?type=case-study&products=terraform')
|
|
35
35
|
})
|
|
@@ -43,18 +43,34 @@ describe('parseUrl', () => {
|
|
|
43
43
|
const url = parseUrl('https://developer.hashicorp.com/terraform')
|
|
44
44
|
expect(url.href).toBe('https://developer.hashicorp.com/terraform')
|
|
45
45
|
})
|
|
46
|
+
|
|
47
|
+
it('detects not allowed schemes', () => {
|
|
48
|
+
const url = parseUrl('ftp://hashicorp.com/example')
|
|
49
|
+
expect(url.linkType).toBe('')
|
|
50
|
+
expect(url.href).toBe('')
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it('returns mailto links', () => {
|
|
54
|
+
const mailtoUrl = parseUrl('mailto:example@hashicorp.com')
|
|
55
|
+
expect(mailtoUrl.href).toBe('mailto:example@hashicorp.com')
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it('returns tel links', () => {
|
|
59
|
+
const telUrl = parseUrl('tel:+1234567890')
|
|
60
|
+
expect(telUrl.href).toBe('tel:+1234567890')
|
|
61
|
+
})
|
|
46
62
|
})
|
|
47
63
|
|
|
48
64
|
describe('hasHashiCorpDestination', () => {
|
|
49
65
|
it('returns true for supported hashicorp domains', () => {
|
|
50
66
|
const isHashiCorpDestination = hasHashiCorpDestination(
|
|
51
|
-
'https://portal.cloud.hashicorp.com/foo'
|
|
67
|
+
'https://portal.cloud.hashicorp.com/foo',
|
|
52
68
|
)
|
|
53
69
|
expect(isHashiCorpDestination).toBe(true)
|
|
54
70
|
})
|
|
55
71
|
it('returns false for external domains', () => {
|
|
56
72
|
const isHashiCorpDestination = hasHashiCorpDestination(
|
|
57
|
-
'https://foo.com/bar'
|
|
73
|
+
'https://foo.com/bar',
|
|
58
74
|
)
|
|
59
75
|
expect(isHashiCorpDestination).toBe(false)
|
|
60
76
|
})
|
|
@@ -39,26 +39,35 @@ export function parseUrl(url: string | undefined): {
|
|
|
39
39
|
href: string
|
|
40
40
|
} {
|
|
41
41
|
if (!url) {
|
|
42
|
-
console.log('No url to parse, returning empty string')
|
|
43
42
|
return { linkType: '', href: '' }
|
|
44
43
|
}
|
|
45
44
|
|
|
45
|
+
// Allow safe relative paths and in-page anchors as-is.
|
|
46
46
|
if (isRelative(url) || isAnchor(url)) {
|
|
47
47
|
return { linkType: 'inbound', href: url }
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
50
|
+
// For absolute URLs, ensure they use an allowed scheme and then classify as internal/outbound.
|
|
51
|
+
try {
|
|
52
|
+
const parsed = new URL(url)
|
|
53
|
+
|
|
54
|
+
if (!isAllowedScheme(parsed.protocol)) {
|
|
55
|
+
console.log(`Disallowed URL scheme in href: ${url}`)
|
|
56
|
+
return { linkType: '', href: '' }
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (isInternal(url)) {
|
|
60
|
+
const { pathname, search, hash } = parsed
|
|
53
61
|
const href = pathname + search + hash
|
|
54
62
|
return { linkType: 'inbound', href }
|
|
55
|
-
} catch (error) {
|
|
56
|
-
console.log(`Invalid href: ${url}`)
|
|
57
|
-
return { linkType: '', href: '' }
|
|
58
63
|
}
|
|
59
|
-
}
|
|
60
64
|
|
|
61
|
-
|
|
65
|
+
// Outbound but still using an allowed scheme.
|
|
66
|
+
return { linkType: 'outbound', href: url }
|
|
67
|
+
} catch (error) {
|
|
68
|
+
console.log(`Invalid href: ${url}`)
|
|
69
|
+
return { linkType: '', href: '' }
|
|
70
|
+
}
|
|
62
71
|
}
|
|
63
72
|
|
|
64
73
|
const isRelative = (url: string) => url.startsWith('/')
|
|
@@ -69,6 +78,15 @@ const isAnchor = (url: string) => url.startsWith('#')
|
|
|
69
78
|
*
|
|
70
79
|
* @param {string} url - The URL to check.
|
|
71
80
|
* @returns {boolean} - Returns true if the link uses a HashiCorp domain, otherwise false.
|
|
81
|
+
* Returns true if the given protocol string is an allowed URL scheme for links.
|
|
82
|
+
* Only http and https are allowed to prevent injection of javascript:, data:, etc.
|
|
83
|
+
*/
|
|
84
|
+
const ALLOWED_SCHEMES = ['http:', 'https:', 'mailto:', 'tel:']
|
|
85
|
+
const isAllowedScheme = (protocol: string): boolean => {
|
|
86
|
+
return ALLOWED_SCHEMES.includes(protocol)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
72
90
|
*/
|
|
73
91
|
|
|
74
92
|
export const isInternal = (url: string): boolean => {
|
package/src/lib/v/index.ts
CHANGED
|
@@ -6,5 +6,11 @@ import { vercelStegaSplit } from '@vercel/stega'
|
|
|
6
6
|
*
|
|
7
7
|
*/
|
|
8
8
|
export const vercelSplit = <VercelString>(
|
|
9
|
-
original: VercelString
|
|
10
|
-
): VercelString =>
|
|
9
|
+
original: VercelString,
|
|
10
|
+
): VercelString => {
|
|
11
|
+
if (typeof original === 'string') {
|
|
12
|
+
return vercelStegaSplit(original).cleaned as VercelString
|
|
13
|
+
} else {
|
|
14
|
+
return original
|
|
15
|
+
}
|
|
16
|
+
}
|
package/tsconfig.json
CHANGED
package/.eslintrc.js
DELETED
package/.stylelintrc.js
DELETED