@hashicorp/react-components 1.1.11 → 1.2.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.
Files changed (29) hide show
  1. package/README.md +1 -1
  2. package/package.json +7 -9
  3. package/postcss.config.js +1 -1
  4. package/src/components/nav-panel/components/flight-icon/index.tsx +4 -2
  5. package/src/components/nav-panel/components/flight-icon/style.module.css +1 -1
  6. package/src/components/nav-panel/components/link/cloud-fill.svg +3 -0
  7. package/src/components/nav-panel/components/link/index.tsx +139 -0
  8. package/src/components/nav-panel/components/link/layers-fill.svg +5 -0
  9. package/src/components/nav-panel/components/link/shield-alert-fill.svg +3 -0
  10. package/src/components/nav-panel/components/link/style.module.css +64 -0
  11. package/src/components/nav-panel/components/link/zap-fill.svg +3 -0
  12. package/src/components/nav-panel/components/link-list/index.tsx +6 -64
  13. package/src/components/nav-panel/components/link-list/style.module.css +0 -49
  14. package/src/components/nav-panel/components/promo/index.tsx +23 -9
  15. package/src/components/nav-panel/components/promo/style.module.css +8 -18
  16. package/src/components/nav-panel/components/text/index.tsx +1 -1
  17. package/src/components/nav-panel/product-panel/index.tsx +47 -7
  18. package/src/components/nav-panel/product-panel/style.module.css +63 -46
  19. package/src/lib/get-localized-link-props/index.test.ts +1 -1
  20. package/src/lib/get-localized-link-props/index.ts +2 -2
  21. package/src/lib/parse-url/index.test.ts +19 -3
  22. package/src/lib/parse-url/index.ts +27 -9
  23. package/src/lib/send-posthog-event/index.ts +1 -1
  24. package/src/lib/supported-locales/index.ts +10 -1
  25. package/src/lib/track-nav-click-event/index.ts +1 -1
  26. package/src/lib/v/index.ts +8 -2
  27. package/tsconfig.json +1 -2
  28. package/.eslintrc.js +0 -4
  29. 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,24 @@
1
1
  {
2
2
  "name": "@hashicorp/react-components",
3
- "version": "1.1.11",
3
+ "version": "1.2.1",
4
4
  "main": "src/index.ts",
5
5
  "scripts": {
6
- "lint": "next-hashicorp 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
- "@hashicorp/platform-product-meta": "^0.1.1",
13
- "@hashicorp/platform-util": "^0.2.0",
14
13
  "@hashicorp/web-mktg-logos": "*",
15
- "@radix-ui/react-navigation-menu": "^1.2.0",
16
- "@radix-ui/react-visually-hidden": "^1.1.0",
14
+ "@radix-ui/react-navigation-menu": "^1.2.14",
15
+ "@radix-ui/react-visually-hidden": "^1.2.4",
17
16
  "@vercel/fetch": "^7.0.0",
18
17
  "@vercel/stega": "^0.1.2",
19
- "posthog-js": "^1.195.0",
20
- "classnames": "^2.3.2"
18
+ "classnames": "^2.3.2",
19
+ "posthog-js": "^1.195.0"
21
20
  },
22
21
  "devDependencies": {
23
- "@hashicorp/platform-cli": "^2.8.0",
24
22
  "@hashicorp/platform-types": "^0.4.0",
25
23
  "@testing-library/jest-dom": "^6.4.2",
26
24
  "@vitejs/plugin-react": "^4.2.1",
package/postcss.config.js CHANGED
@@ -13,7 +13,7 @@ function standardSyntax(plugins) {
13
13
  return _plugins
14
14
  }
15
15
 
16
- const baseConfig = require('@hashicorp/platform-postcss-config')
16
+ const baseConfig = require('@hashicorp/mds-postcss-config')
17
17
 
18
18
  module.exports = {
19
19
  ...baseConfig,
@@ -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={16}
44
- height={16}
45
+ width={size}
46
+ height={size}
45
47
  viewBox="0 0 16 16"
46
48
  xmlns="http://www.w3.org/2000/svg"
47
49
  >
@@ -1,3 +1,3 @@
1
1
  .flight-icon {
2
2
  display: inline-block;
3
- }
3
+ }
@@ -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 Link from 'next/link'
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
- <NavigationMenu.Link asChild>
60
- <Link
61
- className={s.link}
62
- {...getLocalizedLinkProps(link.url, locale)}
63
- onClickCapture={() => {
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
- <div
69
- className={classNames(s.title, {
70
- [s.hashidays]: vercelSplit(theme) === 'hashidays',
71
- [s.hashiconf]: vercelSplit(theme) === 'hashiconf',
72
- })}
73
- >
74
- {title}
75
- </div>
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(--token-color-border-faint);
7
- background-color: var(--token-color-surface-faint);
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(--token-color-foreground-strong);
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(--token-color-focus-action-internal);
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(--token-color-terraform-foreground);
116
+ color: var(--mds-color-terraform-foreground);
127
117
  }
128
118
  }
@@ -22,7 +22,7 @@ const Text = ({
22
22
  className={classNames(
23
23
  `mds-typography-body-${size}`,
24
24
  weight && `mds-typography-font-weight-${weight}`,
25
- className
25
+ className,
26
26
  )}
27
27
  {...rest}
28
28
  >
@@ -3,17 +3,19 @@ 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 '@hashicorp/platform-product-meta'
7
6
  import FlightIcon from '../components/flight-icon'
8
7
  import { Text } from '../components/text'
9
8
  import { trackNavClickEvent } from '../../../lib/track-nav-click-event'
10
9
  import * as NavigationMenu from '@radix-ui/react-navigation-menu'
10
+ import { LinkListProps } from '../components/link-list'
11
+ import NavLink from '../components/link'
11
12
  import s from './style.module.css'
12
13
 
13
14
  const PRODUCT_LOGO_MAP = new Map([
14
15
  ['Terraform', 'terraform-fill'],
15
16
  ['Packer', 'packer-fill-color'],
16
17
  ['Vault', 'vault-fill-color'],
18
+ ['Vault Radar', 'vault-fill-color'],
17
19
  ['Boundary', 'boundary-fill-color'],
18
20
  ['Consul', 'consul-fill-color'],
19
21
  ['Nomad', 'nomad-fill-color'],
@@ -29,9 +31,15 @@ const PRODUCT_LOGO_EXCEPTION_CLASSNAMES = {
29
31
  'HCP Vault Radar': 'hcpVaultRadar',
30
32
  }
31
33
 
34
+ export type ProductCategoriesProps = Array<{
35
+ title: string
36
+ group?: LinkListProps['links'][0]
37
+ products: NavProductProps[]
38
+ }>
39
+
32
40
  export interface ProductsPanelProps {
33
41
  locale?: string
34
- productCategories: any
42
+ productCategories: ProductCategoriesProps
35
43
  promo?: PromoProps
36
44
  sidePanel?: {
37
45
  label: string
@@ -71,10 +79,19 @@ const ProductPanel = ({
71
79
  ) : null}
72
80
  <NavPanel.Column hasSidePanel={Boolean(sidePanel)}>
73
81
  <ul className={s.productCategories}>
74
- {productCategories.map(({ title, products }) => {
82
+ {productCategories.map(({ group, title, products }) => {
75
83
  return (
76
- <li key={title} className={s.productCategory}>
77
- <Text className={s.eyebrow}>{title}</Text>
84
+ <li
85
+ key={title}
86
+ className={classNames(s.productCategory, {
87
+ [s.productCategoryHasGroup]: group,
88
+ })}
89
+ >
90
+ {group ? (
91
+ <NavLink {...group} className={s.productGroupLink} />
92
+ ) : (
93
+ <Text className={s.eyebrow}>{title}</Text>
94
+ )}
78
95
  <ul className={s.productList}>
79
96
  {products.map((product) => {
80
97
  return (
@@ -118,7 +135,7 @@ const ProductPanel = ({
118
135
  <div className={s.iconBackground}>
119
136
  <FlightIcon
120
137
  name={icon}
121
- color="var(--token-color-foreground-faint)"
138
+ color="var(--mds-color-foreground-faint)"
122
139
  />
123
140
  </div>
124
141
  <div className={s.sidePanelItemContent}>
@@ -142,6 +159,16 @@ const ProductPanel = ({
142
159
  )
143
160
  }
144
161
 
162
+ type Products =
163
+ | 'hashicorp'
164
+ | 'boundary'
165
+ | 'consul'
166
+ | 'nomad'
167
+ | 'packer'
168
+ | 'terraform'
169
+ | 'vault'
170
+ | 'vagrant'
171
+ | 'waypoint'
145
172
  export interface NavProductProps {
146
173
  locale?: string
147
174
  url: string
@@ -173,7 +200,20 @@ function NavProduct({ locale, url, product, description }: NavProductProps) {
173
200
  />
174
201
  </div>
175
202
  <div className={s.productTextContent}>
176
- <span>{product}</span>
203
+ {product === 'Vault Radar' ? (
204
+ <div>
205
+ <Text size="200" weight="semibold">
206
+ Vault&nbsp;
207
+ </Text>
208
+ <Text size="200" weight="regular">
209
+ Radar
210
+ </Text>
211
+ </div>
212
+ ) : (
213
+ <Text size="200" weight="semibold">
214
+ {product}
215
+ </Text>
216
+ )}
177
217
  <span className={s.productDesc}>{description}</span>
178
218
  </div>
179
219
  </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(--token-color-border-primary);
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(--token-color-foreground-strong);
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(--token-color-surface-interactive-hover);
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(--token-color-surface-interactive-hover);
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(--token-color-foreground-strong);
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(--token-color-foreground-faint);
184
+ color: var(--mds-color-foreground-faint);
208
185
  }
209
186
 
210
187
  .focusIndicator {
211
188
  &:focus {
212
- outline: 2px solid var(--token-color-focus-action-internal);
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-07) 0 var(--mds-spacing-09)
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(--token-color-surface-faint);
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(--token-color-surface-interactive-hover);
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(--token-color-foreground-high-contrast);
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(--token-color-border-primary);
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(--token-color-foreground-strong);
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(--token-color-foreground-primary);
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(--token-color-terraform-brand);
297
+ fill: var(--mds-color-terraform-brand);
324
298
 
325
299
  @nest [data-theme='dark'] & {
326
- fill: var(--token-color-terraform-foreground);
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(--token-color-vault-brand);
316
+ border: 2px solid var(--mds-color-vault-brand);
343
317
  border-radius: 3px;
344
318
 
345
319
  & use {
346
- fill: var(--token-color-foreground-strong);
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
- if (isInternal(url)) {
51
- try {
52
- const { pathname, search, hash } = new URL(url)
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
- return { linkType: 'outbound', href: url }
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 => {
@@ -14,7 +14,7 @@ import posthog from 'posthog-js'
14
14
  */
15
15
  const sendPosthogEvent = (
16
16
  eventName: string,
17
- properties: Record<string, any>
17
+ properties: Record<string, any>,
18
18
  ) => {
19
19
  posthog.capture(eventName, properties)
20
20
  }
@@ -1 +1,10 @@
1
- export const allSupportedLocales = ['en', 'de', 'es', 'fr', 'ja', 'ko', 'pt']
1
+ export const allSupportedLocales = [
2
+ 'en',
3
+ 'de',
4
+ 'es',
5
+ 'fr',
6
+ 'ja',
7
+ 'ko',
8
+ 'pt',
9
+ 'id',
10
+ ]
@@ -12,7 +12,7 @@ import sendPosthogEvent from '../send-posthog-event'
12
12
  export const trackNavClickEvent = (
13
13
  name: string,
14
14
  href: string,
15
- section: string
15
+ section?: string,
16
16
  ) => {
17
17
  sendPosthogEvent('Nav link click', {
18
18
  name,
@@ -6,5 +6,11 @@ import { vercelStegaSplit } from '@vercel/stega'
6
6
  *
7
7
  */
8
8
  export const vercelSplit = <VercelString>(
9
- original: VercelString
10
- ): VercelString => vercelStegaSplit(original as string).cleaned as 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
@@ -9,6 +9,5 @@
9
9
  ],
10
10
  "types": ["vitest/globals", "@testing-library/jest-dom"]
11
11
  },
12
- "include": ["./**/*"],
13
- "exclude": ["node_modules"]
12
+ "include": ["./**/*"]
14
13
  }
package/.eslintrc.js DELETED
@@ -1,4 +0,0 @@
1
- module.exports = {
2
- root: true,
3
- extends: require.resolve('@hashicorp/platform-cli/config/.eslintrc.js'),
4
- }
package/.stylelintrc.js DELETED
@@ -1,3 +0,0 @@
1
- module.exports = {
2
- extends: ['@hashicorp/platform-cli/config/stylelint.config']
3
- }