@hashicorp/react-components 0.0.0-canary.0299c5d

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 (34) hide show
  1. package/.eslintrc.js +4 -0
  2. package/.stylelintrc.js +3 -0
  3. package/.test/setup-vitest.js +1 -0
  4. package/README.md +3 -0
  5. package/global.d.ts +6 -0
  6. package/package.json +30 -0
  7. package/postcss.config.js +21 -0
  8. package/src/components/nav-panel/components/flight-icon/index.tsx +51 -0
  9. package/src/components/nav-panel/components/flight-icon/style.module.css +3 -0
  10. package/src/components/nav-panel/components/link-list/index.tsx +94 -0
  11. package/src/components/nav-panel/components/link-list/style.module.css +57 -0
  12. package/src/components/nav-panel/components/panel/column/index.tsx +24 -0
  13. package/src/components/nav-panel/components/panel/column/style.module.css +24 -0
  14. package/src/components/nav-panel/components/panel/index.tsx +4 -0
  15. package/src/components/nav-panel/components/panel/root/index.tsx +20 -0
  16. package/src/components/nav-panel/components/panel/root/style.module.css +30 -0
  17. package/src/components/nav-panel/components/promo/index.tsx +92 -0
  18. package/src/components/nav-panel/components/promo/style.module.css +128 -0
  19. package/src/components/nav-panel/components/text/index.tsx +34 -0
  20. package/src/components/nav-panel/index.tsx +6 -0
  21. package/src/components/nav-panel/product-panel/index.tsx +185 -0
  22. package/src/components/nav-panel/product-panel/style.module.css +355 -0
  23. package/src/components/nav-panel/standard-panel/index.tsx +47 -0
  24. package/src/components/nav-panel/standard-panel/style.module.css +11 -0
  25. package/src/lib/get-localized-link-props/index.test.ts +30 -0
  26. package/src/lib/get-localized-link-props/index.ts +45 -0
  27. package/src/lib/parse-url/index.test.ts +69 -0
  28. package/src/lib/parse-url/index.ts +98 -0
  29. package/src/lib/send-posthog-event/index.ts +22 -0
  30. package/src/lib/supported-locales/index.ts +1 -0
  31. package/src/lib/track-nav-click-event/index.ts +22 -0
  32. package/src/lib/v/index.ts +10 -0
  33. package/tsconfig.json +14 -0
  34. package/vitest.config.mts +12 -0
package/.eslintrc.js ADDED
@@ -0,0 +1,4 @@
1
+ module.exports = {
2
+ root: true,
3
+ extends: require.resolve('@hashicorp/platform-cli/config/.eslintrc.js'),
4
+ }
@@ -0,0 +1,3 @@
1
+ module.exports = {
2
+ extends: ['@hashicorp/platform-cli/config/stylelint.config']
3
+ }
@@ -0,0 +1 @@
1
+ import '@testing-library/jest-dom'
package/README.md ADDED
@@ -0,0 +1,3 @@
1
+ # `@hashicorp/react-components`
2
+
3
+ This package contains React components that are built using HDS guidelines that don't align with existing HDS components.
package/global.d.ts ADDED
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Copyright (c) HashiCorp, Inc.
3
+ * SPDX-License-Identifier: MPL-2.0
4
+ */
5
+
6
+ /// <reference types="@hashicorp/platform-types" />
package/package.json ADDED
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "@hashicorp/react-components",
3
+ "version": "0.0.0-canary.0299c5d",
4
+ "main": "src/index.ts",
5
+ "scripts": {
6
+ "lint": "next-hashicorp lint",
7
+ "test": "vitest run",
8
+ "test:watch": "vitest watch"
9
+ },
10
+ "dependencies": {
11
+ "@hashicorp/flight-icons": "^3.7.0",
12
+ "@hashicorp/platform-product-meta": "^0.1.1",
13
+ "@hashicorp/platform-util": "^0.2.0",
14
+ "@hashicorp/web-mktg-logos": "*",
15
+ "@radix-ui/react-navigation-menu": "^1.2.0",
16
+ "@radix-ui/react-visually-hidden": "^1.1.0",
17
+ "@vercel/fetch": "^7.0.0",
18
+ "@vercel/stega": "^0.1.2",
19
+ "posthog-js": "^1.195.0",
20
+ "classnames": "^2.3.2"
21
+ },
22
+ "devDependencies": {
23
+ "@hashicorp/platform-cli": "^2.8.0",
24
+ "@hashicorp/platform-types": "^0.4.0",
25
+ "@testing-library/jest-dom": "^6.4.2",
26
+ "@vitejs/plugin-react": "^4.2.1",
27
+ "nock": "^14.0.0-beta.5",
28
+ "vitest": "^1.4.0"
29
+ }
30
+ }
@@ -0,0 +1,21 @@
1
+ // Next.js allows for plugins to be specified as an array of arrays, but this
2
+ // isn't supported by Vitest, so use a helper function to convert the array of
3
+ // arrays to an object.
4
+ function standardSyntax(plugins) {
5
+ const _plugins = {}
6
+ plugins.forEach((p) => {
7
+ if (Array.isArray(p)) {
8
+ _plugins[p[0]] = p[1]
9
+ } else {
10
+ _plugins[p] = {}
11
+ }
12
+ })
13
+ return _plugins
14
+ }
15
+
16
+ const baseConfig = require('@hashicorp/platform-postcss-config')
17
+
18
+ module.exports = {
19
+ ...baseConfig,
20
+ plugins: standardSyntax(baseConfig.plugins),
21
+ }
@@ -0,0 +1,51 @@
1
+ import svgSprite from '@hashicorp/flight-icons/svg-sprite/svg-sprite.svg'
2
+ import classNames from 'classnames'
3
+ import { useId, type SVGProps } from 'react'
4
+ import s from './style.module.css'
5
+
6
+ interface Props extends SVGProps<SVGSVGElement> {
7
+ /**
8
+ * The name of the icon you wish to use. If the value does not match an
9
+ * existing icon name, an error will be thrown. Search for existing icon
10
+ * names in the Icon library.
11
+ */
12
+ name: string
13
+
14
+ /**
15
+ * The `color` prop can be used to change the color. It works by setting
16
+ * the value of the icon SVG’s fill property.
17
+ */
18
+ color?: string
19
+
20
+ className?: string
21
+ }
22
+
23
+ // When an SVG file is imported in Next.js, it is an object with a `src`
24
+ // property. However, tools like Webpack and Vite default to returning a URL.
25
+ // This runtime check allows the component to work in all of these cases.
26
+ const svgSpriteSrc =
27
+ typeof svgSprite === 'string' ? svgSprite : (svgSprite as { src: string }).src
28
+
29
+ export default function FlightIcon({
30
+ name,
31
+ color = 'currentColor',
32
+ className,
33
+ ...rest
34
+ }: Props) {
35
+ const iconId = useId()
36
+ return (
37
+ <svg
38
+ className={classNames(s['flight-icon'], className)}
39
+ {...rest}
40
+ aria-hidden="true"
41
+ fill={color}
42
+ id={iconId}
43
+ width={16}
44
+ height={16}
45
+ viewBox="0 0 16 16"
46
+ xmlns="http://www.w3.org/2000/svg"
47
+ >
48
+ <use href={`${svgSpriteSrc}#flight-${name}-16`}></use>
49
+ </svg>
50
+ )
51
+ }
@@ -0,0 +1,3 @@
1
+ .flight-icon {
2
+ display: block;
3
+ }
@@ -0,0 +1,94 @@
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'
6
+ import s from './style.module.css'
7
+
8
+ export interface LinkListProps {
9
+ locale?: string
10
+ links: {
11
+ title: string
12
+ url: string
13
+ icon?: string
14
+ description?: string
15
+ iconGradient?: string
16
+ }[]
17
+ linkClickHandler?: (title: string, url: string) => void
18
+ }
19
+
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
+ export default function LinkList({
42
+ locale,
43
+ links,
44
+ linkClickHandler,
45
+ }: LinkListProps) {
46
+ return (
47
+ <ul className={s.linkList}>
48
+ {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
+ return (
58
+ <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>
89
+ </li>
90
+ )
91
+ })}
92
+ </ul>
93
+ )
94
+ }
@@ -0,0 +1,57 @@
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(--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
+ }
@@ -0,0 +1,24 @@
1
+ import classNames from 'classnames'
2
+ import s from './style.module.css'
3
+
4
+ export interface NavColumnProps {
5
+ className?: string
6
+ children: React.ReactNode
7
+ hasSidePanel?: boolean
8
+ }
9
+
10
+ export default function Column({
11
+ className,
12
+ children,
13
+ hasSidePanel,
14
+ }: NavColumnProps) {
15
+ return (
16
+ <div
17
+ className={classNames(s.column, className, {
18
+ [s.hasSide]: hasSidePanel,
19
+ })}
20
+ >
21
+ {children}
22
+ </div>
23
+ )
24
+ }
@@ -0,0 +1,24 @@
1
+ @custom-media --dev-dot-viewport (min-width: 924px);
2
+
3
+ .column {
4
+ position: relative;
5
+ display: flex;
6
+ flex-direction: column;
7
+ justify-content: flex-start;
8
+ min-width: 190px;
9
+ padding: 0;
10
+ margin: 0 var(--mds-spacing-07);
11
+ border-radius: var(--mds-spacing-03);
12
+
13
+ @media (--large) {
14
+ &:not(.hasSide) {
15
+ margin: 0;
16
+ }
17
+ }
18
+
19
+ @media (--dev-dot-viewport) {
20
+ &.hasSide {
21
+ margin: 0;
22
+ }
23
+ }
24
+ }
@@ -0,0 +1,4 @@
1
+ import Root from './root'
2
+ import Column from './column'
3
+
4
+ export { Root, Column }
@@ -0,0 +1,20 @@
1
+ import classNames from 'classnames'
2
+ import s from './style.module.css'
3
+
4
+ interface RootProps {
5
+ className?: string
6
+ children: React.ReactNode
7
+ hasSidePanel?: boolean
8
+ }
9
+
10
+ export default function Root({ className, children, hasSidePanel }: RootProps) {
11
+ return (
12
+ <div
13
+ className={classNames(s.root, className, {
14
+ [s.hasSide]: hasSidePanel,
15
+ })}
16
+ >
17
+ {children}
18
+ </div>
19
+ )
20
+ }
@@ -0,0 +1,30 @@
1
+ @custom-media --dev-dot-viewport (min-width: 924px);
2
+
3
+ .root {
4
+ width: 100%;
5
+ display: flex;
6
+ flex-direction: column;
7
+ justify-content: space-between;
8
+ /* stylelint-disable-next-line unit-no-unknown */
9
+ min-height: calc(100dvh - var(--mds-spacing-12));
10
+
11
+ @media (--large) {
12
+ &:not(.hasSide) {
13
+ justify-content: flex-start;
14
+ height: 100%;
15
+ min-height: fit-content;
16
+ padding: var(--mds-spacing-05) var(--mds-spacing-04);
17
+ gap: var(--mds-spacing-04);
18
+ }
19
+ }
20
+
21
+ @media (--dev-dot-viewport) {
22
+ &.hasSide {
23
+ justify-content: flex-start;
24
+ height: 100%;
25
+ min-height: fit-content;
26
+ padding: var(--mds-spacing-05) var(--mds-spacing-04);
27
+ gap: var(--mds-spacing-04);
28
+ }
29
+ }
30
+ }
@@ -0,0 +1,92 @@
1
+ import { useCallback } from 'react'
2
+ import classNames from 'classnames'
3
+ import { vercelSplit } from '../../../../lib/v'
4
+ import Link from 'next/link'
5
+ import { getLocalizedLinkProps } from '../../../../lib/get-localized-link-props'
6
+ import { trackNavClickEvent } from '../../../../lib/track-nav-click-event'
7
+ import FlightIcon from '../flight-icon'
8
+ import * as NavigationMenu from '@radix-ui/react-navigation-menu'
9
+ import s from './style.module.css'
10
+
11
+ export interface PromoProps {
12
+ locale?: string
13
+ title: string
14
+ description: string
15
+ linkUrl: string
16
+ linkTitle: string
17
+ slug?: string
18
+ icon?: string
19
+ theme: string
20
+ hasSidePanel?: boolean
21
+ isPromoOnTop?: boolean
22
+ }
23
+
24
+ export default function Promo({
25
+ locale,
26
+ title,
27
+ description,
28
+ linkUrl,
29
+ linkTitle,
30
+ slug,
31
+ icon,
32
+ theme,
33
+ hasSidePanel,
34
+ isPromoOnTop = false,
35
+ }: PromoProps) {
36
+ const handleClick = useCallback((title, linkUrl, linkTitle, slug) => {
37
+ if (window && window.analytics) {
38
+ window.analytics.track('Nav Promo', {
39
+ url: linkUrl,
40
+ title: title,
41
+ callout: linkTitle,
42
+ })
43
+
44
+ trackNavClickEvent(title, linkUrl, slug)
45
+ }
46
+ }, [])
47
+
48
+ return (
49
+ <div
50
+ className={classNames(s.promo, {
51
+ [s.hasSide]: hasSidePanel,
52
+ [s.noSide]: !hasSidePanel,
53
+ [s.topPromo]: isPromoOnTop,
54
+ })}
55
+ >
56
+ {icon && (
57
+ <div
58
+ className={classNames(s.icon, {
59
+ [s.hashidays]: vercelSplit(theme) === 'hashidays',
60
+ [s.hashiconf]: vercelSplit(theme) === 'hashiconf',
61
+ })}
62
+ >
63
+ <FlightIcon name={vercelSplit(icon)} />
64
+ </div>
65
+ )}
66
+ <div className={s.rightContent}>
67
+ <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>
76
+ <div className={s.description}>{description}</div>
77
+ </div>
78
+ <NavigationMenu.Link asChild>
79
+ <Link
80
+ locale={locale ? locale : undefined}
81
+ {...getLocalizedLinkProps(linkUrl, locale)}
82
+ className={s.link}
83
+ onClickCapture={() => handleClick(title, linkUrl, linkTitle, slug)}
84
+ >
85
+ {linkTitle}
86
+ <FlightIcon name="external-link" />
87
+ </Link>
88
+ </NavigationMenu.Link>
89
+ </div>
90
+ </div>
91
+ )
92
+ }
@@ -0,0 +1,128 @@
1
+ @custom-media --dev-dot-viewport (min-width: 924px);
2
+
3
+ .promo {
4
+ display: flex;
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);
10
+ flex-direction: row;
11
+ gap: var(--mds-spacing-04);
12
+ margin: var(--mds-spacing-07);
13
+ margin-bottom: var(--mds-spacing-09);
14
+
15
+ @media (--large) {
16
+ &.topPromo {
17
+ margin-bottom: var(--mds-spacing-05);
18
+ }
19
+ &.noSide {
20
+ margin: 0;
21
+ width: min-content;
22
+ min-width: 100%;
23
+ }
24
+ }
25
+
26
+ @media (--dev-dot-viewport) {
27
+ &.topPromo {
28
+ margin-bottom: var(--mds-spacing-05);
29
+ }
30
+ &.hasSide {
31
+ &.topPromo {
32
+ margin-bottom: var(--mds-spacing-05);
33
+ }
34
+
35
+ margin: 0;
36
+ width: min-content;
37
+ min-width: 100%;
38
+ }
39
+ }
40
+ }
41
+
42
+ .topPromo {
43
+ margin-bottom: 0;
44
+ }
45
+
46
+ .rightContent {
47
+ display: flex;
48
+ flex-wrap: wrap;
49
+ gap: var(--mds-spacing-03);
50
+ width: 100%;
51
+ justify-content: space-between;
52
+ }
53
+
54
+ .title {
55
+ font-weight: var(--mds-typography-font-weight-semibold);
56
+ }
57
+
58
+ .description {
59
+ font-size: var(--mds-typography-body-200-font-size);
60
+ font-weight: var(--mds-typography-font-weight-regular);
61
+ margin-top: var(--mds-spacing-01);
62
+ }
63
+
64
+ .icon {
65
+ display: none;
66
+
67
+ @media (--large) {
68
+ @nest .noSide & {
69
+ display: block;
70
+ margin-top: 3px;
71
+ }
72
+ }
73
+
74
+ @media (--dev-dot-viewport) {
75
+ @nest .hasSide & {
76
+ display: block;
77
+ margin-top: 3px;
78
+ }
79
+ }
80
+ }
81
+
82
+ .link {
83
+ color: var(--token-color-foreground-strong);
84
+ display: flex;
85
+ flex-direction: row;
86
+ align-items: center;
87
+ gap: var(--mds-spacing-02);
88
+
89
+ &:hover {
90
+ text-decoration: underline;
91
+ }
92
+
93
+ &:focus {
94
+ outline: 2px solid var(--token-color-focus-action-internal);
95
+ outline-style: auto;
96
+ }
97
+ }
98
+
99
+ .hashidays {
100
+ background: linear-gradient(
101
+ 90deg,
102
+ #7b54b6 0%,
103
+ #4c69d8 74.77%,
104
+ #60d0dc 115.93%
105
+ );
106
+ -webkit-background-clip: text;
107
+ -webkit-text-fill-color: transparent;
108
+ max-width: fit-content;
109
+
110
+ & svg {
111
+ color: #7956b9;
112
+ }
113
+ }
114
+
115
+ .hashiconf {
116
+ background: linear-gradient(
117
+ to right,
118
+ var(--token-color-terraform-foreground),
119
+ var(--token-color-vagrant-foreground)
120
+ );
121
+ -webkit-background-clip: text;
122
+ -webkit-text-fill-color: transparent;
123
+ max-width: fit-content;
124
+
125
+ & svg {
126
+ color: var(--token-color-terraform-foreground);
127
+ }
128
+ }
@@ -0,0 +1,34 @@
1
+ import classNames from 'classnames'
2
+ import { type ReactNode, type HTMLAttributes } from 'react'
3
+
4
+ type TextSize = '200' | '300' | '400'
5
+ type TextWeight = 'regular' | 'medium' | 'semibold' | 'bold'
6
+
7
+ interface TextProps extends HTMLAttributes<HTMLSpanElement> {
8
+ size?: TextSize
9
+ weight?: TextWeight
10
+ children: ReactNode
11
+ }
12
+
13
+ const Text = ({
14
+ size = '300',
15
+ weight,
16
+ children,
17
+ className,
18
+ ...rest
19
+ }: TextProps) => {
20
+ return (
21
+ <span
22
+ className={classNames(
23
+ `mds-typography-body-${size}`,
24
+ weight && `mds-typography-font-weight-${weight}`,
25
+ className
26
+ )}
27
+ {...rest}
28
+ >
29
+ {children}
30
+ </span>
31
+ )
32
+ }
33
+
34
+ export { Text }
@@ -0,0 +1,6 @@
1
+ import ProductPanel, { type NavProductProps } from './product-panel'
2
+ import StandardPanel, { type NavItem } from './standard-panel'
3
+ import { PromoProps } from './components/promo'
4
+
5
+ export { ProductPanel, StandardPanel }
6
+ export type { NavProductProps, PromoProps, NavItem }