@dbosoft/nextjs-uicore 1.0.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.
Files changed (38) hide show
  1. package/.eslintrc.js +4 -0
  2. package/CHANGELOG.md +12 -0
  3. package/package.json +40 -0
  4. package/src/global.d.ts +1 -0
  5. package/src/head/global.d.ts +1 -0
  6. package/src/head/index.tsx +119 -0
  7. package/src/subnav/helpers/useStuckRef.ts +50 -0
  8. package/src/subnav/index.tsx +133 -0
  9. package/src/subnav/partials/CtaLinks/github-stars-link/formatStarCount/index.test.js +25 -0
  10. package/src/subnav/partials/CtaLinks/github-stars-link/formatStarCount/index.ts +17 -0
  11. package/src/subnav/partials/CtaLinks/github-stars-link/index.tsx +84 -0
  12. package/src/subnav/partials/CtaLinks/github-stars-link/parseGithubUrl/index.test.js +25 -0
  13. package/src/subnav/partials/CtaLinks/github-stars-link/parseGithubUrl/index.ts +25 -0
  14. package/src/subnav/partials/CtaLinks/icons/github.svg +4 -0
  15. package/src/subnav/partials/CtaLinks/index.tsx +46 -0
  16. package/src/subnav/partials/MenuItemsDefault/index.tsx +63 -0
  17. package/src/subnav/partials/MenuItemsDefault/style.module.scss +71 -0
  18. package/src/subnav/partials/MenuItemsOverflow/index.tsx +50 -0
  19. package/src/subnav/partials/MenuItemsOverflow/style.module.scss +51 -0
  20. package/src/subnav/partials/TitleLink/index.tsx +25 -0
  21. package/src/subnav/partials/nav-item-text/index.tsx +29 -0
  22. package/src/subnav/partials/nav-item-text/style.module.scss +19 -0
  23. package/src/subnav/style.module.scss +13 -0
  24. package/src/tabs/hooks/use-scroll-left.ts +27 -0
  25. package/src/tabs/hooks/use-window-size.js +33 -0
  26. package/src/tabs/icons/chevron-right.svg +1 -0
  27. package/src/tabs/icons/tooltip.svg +1 -0
  28. package/src/tabs/index.tsx +102 -0
  29. package/src/tabs/partials/tab-trigger/index.tsx +66 -0
  30. package/src/tabs/partials/tab-trigger/style.module.scss +69 -0
  31. package/src/tabs/partials/tab-triggers/index.tsx +241 -0
  32. package/src/tabs/partials/tab-triggers/style.module.scss +193 -0
  33. package/src/tabs/partials/tooltip/index.tsx +112 -0
  34. package/src/tabs/partials/tooltip/style.module.scss +38 -0
  35. package/src/tabs/provider.js +18 -0
  36. package/src/tabs/style.module.css +4 -0
  37. package/src/tabs/utils/smooth-scroll.js +88 -0
  38. package/tsconfig.json +8 -0
package/.eslintrc.js ADDED
@@ -0,0 +1,4 @@
1
+ /** @type {import("eslint").Linter.Config} */
2
+ module.exports = {
3
+ extends: ["@dbosoft/eslint-config/react.js"],
4
+ };
package/CHANGELOG.md ADDED
@@ -0,0 +1,12 @@
1
+ # @dbosoft/nextjs-uicore
2
+
3
+ ## 1.0.0
4
+
5
+ ### Major Changes
6
+
7
+ - restructured nextjs components
8
+
9
+ ### Patch Changes
10
+
11
+ - Updated dependencies
12
+ - @dbosoft/react-uicore@1.0.0
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "@dbosoft/nextjs-uicore",
3
+ "description": "Core UI components for Next.js",
4
+ "author": "dbosoft",
5
+ "version": "1.0.0",
6
+ "sideEffects": false,
7
+ "license": "MIT",
8
+ "exports": {
9
+ ".": "./src",
10
+ "./head": "./src/head/index.tsx",
11
+ "./tabs": "./src/tabs/index.tsx",
12
+ "./subnav": "./src/subnav/index.tsx"
13
+ },
14
+ "devDependencies": {
15
+ "@types/react": "^18.2.46",
16
+ "@types/react-dom": "^18.2.18",
17
+ "eslint": "^8.56.0",
18
+ "next": "^14.0.4",
19
+ "react": "^18.2.0",
20
+ "typescript": "^5.3.3",
21
+ "@dbosoft/eslint-config": "1.0.0",
22
+ "@dbosoft/web-types": "1.0.0",
23
+ "@dbosoft/typescript-config": "1.0.0"
24
+ },
25
+ "publishConfig": {
26
+ "access": "public"
27
+ },
28
+ "dependencies": {
29
+ "@headlessui/react": "^1.4.3",
30
+ "@heroicons/react": "^1.0.5",
31
+ "classnames": "^2.3.1",
32
+ "isomorphic-unfetch": "^4.0.2",
33
+ "unfetch": "^4.2.0",
34
+ "@dbosoft/react-uicore": "1.0.0"
35
+ },
36
+ "scripts": {
37
+ "lint": "eslint . --max-warnings 0",
38
+ "clean": "rimraf .turbo && rimraf node_modules && rimraf dist"
39
+ }
40
+ }
@@ -0,0 +1 @@
1
+ /// <reference types="@dbosoft/web-types" />
@@ -0,0 +1 @@
1
+ /// <reference types="@dbosoft/web-types" />
@@ -0,0 +1,119 @@
1
+ import Head from 'next/head'
2
+
3
+ export default function DBOHead(props: DBOHeadProps): React.ReactElement {
4
+ return (
5
+ <Head>
6
+ {whenString(props.title, <title>{props.title}</title>)}
7
+ <meta httpEquiv="x-ua-compatible" content="ie=edge" />
8
+ <meta property="og:locale" content="en_US" key="og:locale" />
9
+ <meta property="og:type" content="website" key="og:type" />
10
+ <meta
11
+ property="article:publisher"
12
+ content="https://www.facebook.com/dbosoft/"
13
+ key="article:publisher"
14
+ />
15
+ <meta name="twitter:site" content="@dbosoft" key="twitter:site" />
16
+ <meta
17
+ name="twitter:card"
18
+ content={props.twitterCard || 'summary_large_image'}
19
+ key="twitter:card"
20
+ />
21
+ <meta name="theme-color" content="#000" key="themeColor" />
22
+ {whenString(
23
+ props.description,
24
+ <meta
25
+ name="description"
26
+ property="og:description"
27
+ content={props.description}
28
+ key="description"
29
+ />
30
+ )}
31
+ {whenString(
32
+ props.siteName,
33
+ <meta
34
+ property="og:site_name"
35
+ content={props.siteName}
36
+ key="og:site_name"
37
+ />
38
+ )}
39
+ {whenString(
40
+ props.pageName,
41
+ <meta property="og:title" content={props.pageName} key="og:title" />
42
+ )}
43
+ {whenString(
44
+ props.image,
45
+ <meta property="og:image" content={props.image} key="og:image" />
46
+ )}
47
+ {whenString(
48
+ props.canonicalUrl,
49
+ <link rel="canonical" key="canonical" href={props.canonicalUrl} />
50
+ )}
51
+ {whenArray(props.preload, ({ href, ...linkProps }: {href:string}) => (
52
+ <link href={href} {...linkProps} rel="preload" key={href} />
53
+ ))}
54
+ {whenArray(props.icon, ({ href, ...linkProps }: {href:string}) => (
55
+ <link href={href} {...linkProps} rel="icon" key={href} />
56
+ ))}
57
+ {whenArray(props.stylesheet, ({ href, ...linkProps }: {href:string}) => (
58
+ <link href={href} {...linkProps} rel="stylesheet" key={href} />
59
+ ))}
60
+ {props.children}
61
+ </Head>
62
+ )
63
+ }
64
+
65
+ /** Return a map of the value using the callback if it is an Array. */
66
+ const whenArray = (value : any, mapFn: $TSFixMe) =>
67
+ Array.isArray(value) ? value.map(mapFn) : null
68
+
69
+ /** Return the value if it is a String */
70
+ const whenString = (value : any, returnValue: any) =>
71
+ typeof value === 'string' ? returnValue : null
72
+
73
+ // -----
74
+ // Types
75
+ // -----
76
+
77
+ interface DBOHeadProps {
78
+ canonicalUrl?: string
79
+ children?: React.ReactNode
80
+ description?: string
81
+ icon?: {
82
+ [key: string]: unknown
83
+ href: string
84
+ sizes?: string
85
+ type?: string
86
+ }[]
87
+ image?: string
88
+ pageName?: string
89
+ preload?: {
90
+ [key: string]: unknown
91
+ as: asProp
92
+ href: string
93
+ type?: string
94
+ }[]
95
+ siteName?: string
96
+ stylesheet?: {
97
+ [key: string]: unknown
98
+ href: string
99
+ media?: string
100
+ }[]
101
+ title?: string
102
+ twitterCard?: TwitterCardProp
103
+ }
104
+
105
+ type TwitterCardProp = 'summary' | 'summary_large_image'
106
+
107
+ type asProp =
108
+ | 'audio'
109
+ | 'document'
110
+ | 'embed'
111
+ | 'fetch'
112
+ | 'font'
113
+ | 'image'
114
+ | 'object'
115
+ | 'script'
116
+ | 'style'
117
+ | 'track'
118
+ | 'video'
119
+ | 'worker'
@@ -0,0 +1,50 @@
1
+ "use client"
2
+
3
+ import type { DependencyList} from 'react';
4
+ import { useCallback, useState } from 'react'
5
+
6
+ const IntersectionObserver =
7
+ (typeof window !== 'undefined' && window.IntersectionObserver) || null
8
+ const intersectionOpts = { threshold: [1] }
9
+
10
+ /*
11
+
12
+ Stuck-ness is determined by whether a sticky target element has intersected the
13
+ top boundary of its viewport.
14
+
15
+ Stuck-ness is checked on intersection observation.
16
+
17
+ IntersectionObserver Compatibility (https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver#Browser_compatibility):
18
+
19
+ - Chrome 51+
20
+ - Edge 15+
21
+ - Firefox 55+
22
+ - Safari 12.1+
23
+
24
+ Internet Explorer 11 gracefully skips over this functionality, as it does not
25
+ support `IntersectionObserver` or CSS `position: sticky`.
26
+
27
+ */
28
+
29
+ export default function useStuckRef(deps: DependencyList) {
30
+ const [isStuck, setStuck] = useState(false)
31
+
32
+ const stuckRef = useCallback((target: Element | null) => {
33
+ if (target && IntersectionObserver) {
34
+ const intersectionObserver = new IntersectionObserver(([entry]) => {
35
+ const nowIsStuck = entry.intersectionRatio < 1
36
+ setStuck(nowIsStuck);
37
+
38
+ }, intersectionOpts)
39
+
40
+ intersectionObserver.observe(target)
41
+
42
+ return intersectionObserver.disconnect.bind(intersectionObserver)
43
+ }
44
+ return () => { };
45
+ }, deps);
46
+
47
+
48
+
49
+ return { isStuck, stuckRef }
50
+ }
@@ -0,0 +1,133 @@
1
+ import { Disclosure } from '@headlessui/react'
2
+ import { MenuIcon, XIcon } from '@heroicons/react/outline'
3
+ import classNames from 'classnames'
4
+ import type { LinkType, Variant } from '@dbosoft/react-uicore/linkbutton'
5
+ import MenuItemsOverflow from './partials/MenuItemsOverflow'
6
+ import MenuItemsDefault from './partials/MenuItemsDefault'
7
+ import CtaLinks from './partials/CtaLinks'
8
+ import useStuckRef from './helpers/useStuckRef'
9
+ import TitleLink from './partials/TitleLink'
10
+ import style from './style.module.scss'
11
+
12
+ export type MenuItem = 'divider' | ILinkMenuItem;
13
+
14
+ export interface IDropDownMenuItem {
15
+ text: string,
16
+ submenu: ILinkMenuItem[]
17
+ }
18
+
19
+ export interface ILinkMenuItem {
20
+ text: string,
21
+ url: string,
22
+ type: 'inbound' | 'outbound' | 'anchor'
23
+ active?: boolean
24
+ }
25
+
26
+
27
+ export interface ICtaItem {
28
+ text: string,
29
+ url: string,
30
+ variant?: Variant
31
+ linktype?: LinkType
32
+ className?: string,
33
+ }
34
+
35
+ interface ISubNavProps {
36
+ titleLink?: ILinkMenuItem,
37
+ titleContent?: JSX.Element,
38
+ ctaLinks: ICtaItem[],
39
+ hideGithubStars: boolean,
40
+ menuItems: MenuItem[],
41
+ menuItemsAlign: 'left' | 'right',
42
+ className?: string
43
+ }
44
+
45
+ function Subnav({
46
+ className,
47
+ titleLink,
48
+ ctaLinks = [],
49
+ hideGithubStars,
50
+ menuItems,
51
+ menuItemsAlign = `right`,
52
+ titleContent
53
+ }: ISubNavProps) {
54
+
55
+ const { isStuck, stuckRef } = useStuckRef([])
56
+
57
+ return (
58
+ <div className={classNames(style.root, `bg-white`, className, {
59
+ [style.isSticky]: isStuck,
60
+ })} ref={stuckRef}>
61
+ <Disclosure as="nav" >
62
+ {({ open }) => (
63
+ <>
64
+ <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
65
+ <div className="flex justify-between h-16">
66
+ <div className={classNames(`flex grow`, menuItemsAlign == `right` ? ` justify-between` : ``)}>
67
+
68
+ <div className="flex-shrink-0 flex items-center ">
69
+ {titleLink &&<TitleLink
70
+ text={titleLink.text}
71
+
72
+ url={titleLink.url} >{titleContent}</TitleLink>}
73
+ {titleContent}
74
+ </div>
75
+ <div className="hidden sm:-my-px sm:ml-6 sm:flex">
76
+ <MenuItemsDefault
77
+ menuItems={menuItems}
78
+ />
79
+ <CtaLinks
80
+ hideGithubStars={hideGithubStars}
81
+ isInDropdown={false}
82
+ links={ctaLinks}
83
+ />
84
+ </div>
85
+ </div>
86
+ <div className="-mr-2 flex items-center sm:hidden ">
87
+ {/* Mobile menu button */}
88
+ <Disclosure.Button className="bg-white inline-flex items-center justify-center p-2 rounded-md text-gray-400 hover:text-gray-500 hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-secondary">
89
+ <span className="sr-only">Open main menu</span>
90
+ {open ? (
91
+ <XIcon aria-hidden="true" className="block h-6 w-6" />
92
+ ) : (
93
+ <MenuIcon aria-hidden="true" className="block h-6 w-6" />
94
+ )}
95
+ </Disclosure.Button>
96
+ </div>
97
+ </div>
98
+ </div>
99
+
100
+ <Disclosure.Panel className="sm:hidden">
101
+ <div className="pt-2 pb-3 space-y-1">
102
+ <MenuItemsOverflow
103
+ ctaLinks={ctaLinks}
104
+ hideGithubStars={hideGithubStars}
105
+ menuItems={menuItems}
106
+ />
107
+ {/* {navigation.map((item) => (
108
+ <Disclosure.Button
109
+ key={item.name}
110
+ as="a"
111
+ href={item.href}
112
+ className={classNames(
113
+ item.current
114
+ ? 'bg-indigo-50 border-indigo-500 text-indigo-700'
115
+ : 'border-transparent text-gray-600 hover:bg-gray-50 hover:border-gray-300 hover:text-gray-800',
116
+ 'block pl-3 pr-4 py-2 border-l-4 text-base font-medium'
117
+ )}
118
+ aria-current={item.current ? 'page' : undefined}
119
+ >
120
+ {item.name}
121
+ </Disclosure.Button>
122
+ ))}
123
+ */} </div>
124
+
125
+ </Disclosure.Panel>
126
+ </>
127
+ )}
128
+ </Disclosure>
129
+ </div>
130
+ )
131
+ }
132
+
133
+ export default Subnav
@@ -0,0 +1,25 @@
1
+ import formatStarCount from './index.js'
2
+
3
+ describe('<Subnav /> - formatStarCount', () => {
4
+ it('should return zero for an undefined, null, or zero count', () => {
5
+ expect(formatStarCount(undefined)).toBe(false)
6
+ expect(formatStarCount(null)).toBe(false)
7
+ expect(formatStarCount(0)).toBe(false)
8
+ })
9
+
10
+ it('should return numbers 1 to 999 without formatting', () => {
11
+ expect(formatStarCount(1)).toBe('1')
12
+ expect(formatStarCount(999)).toBe('999')
13
+ })
14
+
15
+ it('should abbreviate numbers in the tens of thousands with "k" and a single decimal point', () => {
16
+ expect(formatStarCount(1003)).toBe('1.0k')
17
+ expect(formatStarCount(1280)).toBe('1.2k')
18
+ expect(formatStarCount(15630)).toBe('15.6k')
19
+ })
20
+
21
+ it('should abbreviate numbers in the hundreds of thousands with "k" and no decimal point', () => {
22
+ expect(formatStarCount(100000)).toBe('100k')
23
+ expect(formatStarCount(156783)).toBe('156k')
24
+ })
25
+ })
@@ -0,0 +1,17 @@
1
+ /*
2
+ * Given:
3
+ * starCount (int)
4
+ * Return:
5
+ * Formatted string, to match GitHub's typical display of star counts,
6
+ * that is, expressed as thousands of stars
7
+ * Or returns false for falsy starCount values
8
+ */
9
+ function formatStarCount(starCount: number) {
10
+ if (!starCount || starCount <= 0) return false
11
+ if (starCount < 1000) return `${starCount}`
12
+ const thousands = Math.floor(starCount / 100.0) / 10.0
13
+ if (starCount < 100000) return `${thousands.toFixed(1)}k`
14
+ return `${Math.floor(thousands)}k`
15
+ }
16
+
17
+ export default formatStarCount
@@ -0,0 +1,84 @@
1
+ "use client";
2
+
3
+ import React, { useEffect, useState } from 'react'
4
+ import fetch from 'isomorphic-unfetch'
5
+ import type { IconObject } from '@dbosoft/react-uicore/linkbutton';
6
+ import LinkButton from '@dbosoft/react-uicore/linkbutton';
7
+ import { StarIcon } from '@heroicons/react/solid'
8
+ import GithubIcon from '../icons/github.svg'
9
+ import formatStarCount from './formatStarCount'
10
+ import parseGithubUrl from './parseGithubUrl'
11
+
12
+ function GithubStarsButton({ url, hideGithubStars }: { url: string, hideGithubStars: boolean }) {
13
+ const [starCount, setStarCount] = useState(-1)
14
+
15
+ useEffect(() => {
16
+ if (hideGithubStars) { setStarCount(0); return; }
17
+ const parseResult = parseGithubUrl(url);
18
+ const { org, repo } = parseResult !== false ? parseResult : { org: undefined, repo: undefined };
19
+
20
+ if (!org || !repo) { setStarCount(0); return; }
21
+ const githubApiUrl = `https://api.github.com/repos/${org}/${repo}`
22
+ fetch(githubApiUrl)
23
+ .then((response) => {
24
+ response.json().then((data) => {
25
+ // Github's rate limit for unauthenticated requests is 60 per hour
26
+ // When the limit is hit, data.stargazers_count is undefined,
27
+ // and setStarCount falls back to not showing the star count
28
+ setStarCount(data.stargazers_count)
29
+ // Warn if this limit is hit, to avoid otherwise confusing behavior
30
+ // We're still using the response to provide a documentation link
31
+ if (!data.stargazers_count) {
32
+ const { headers } = response
33
+ if (headers.get('x-ratelimit-remaining') === '0') {
34
+ const resetAtSeconds = parseInt(headers.get('x-ratelimit-reset') || '')
35
+ const resetDate = new Date(resetAtSeconds * 1000)
36
+ const rateLimit = headers.get('x-ratelimit-limit')
37
+ console.warn(
38
+ `⭐ Stargazers count could not be fetched. Rate limit exceeded for unauthenticated GitHub API. Limit will be reset to ${rateLimit} at ${resetDate}. See ${data.documentation_url} for more details.`
39
+ )
40
+ } else {
41
+ console.warn(
42
+ `Request for stargazers was successful, but the returned value was undefined or falsy. This might be because the repo has no stars, or it might be a different issue.`
43
+ )
44
+ }
45
+ }
46
+ })
47
+ })
48
+ .catch((err) => {
49
+ setStarCount(0)
50
+ console.warn(JSON.stringify(err, null, 2))
51
+ })
52
+ }, [hideGithubStars, url])
53
+
54
+ const isLoadingStarCount = starCount === -1
55
+ const isFailedStarCount = formatStarCount(starCount) === false
56
+
57
+ const showStarCount = !hideGithubStars && (isLoadingStarCount || !isFailedStarCount);
58
+
59
+ const icon: IconObject = {
60
+ position: 'right',
61
+ svg: GithubIcon,
62
+ }
63
+
64
+ return (
65
+ <LinkButton icon={!showStarCount ? icon : undefined} label='Github' text={!showStarCount ? "Github" : ""}
66
+ variant='neutral' >
67
+ {showStarCount ? <div className="inline-flex">
68
+ <GithubIcon className="w-5 mr-2" />
69
+ <div className='-my-2 mx-2 h-9 block bg-gray-300' style={{ width: "1px" }} />
70
+
71
+ <span className="inline-flex gap-1 items-center">
72
+ <StarIcon className='w-5 fill-yellow-500' />
73
+ <span className="g-type-body-small-strong" data-testid="github-stars">
74
+ {formatStarCount(starCount) || <span>&mdash;</span>}
75
+ </span>
76
+ </span>
77
+
78
+ </div> : null}
79
+ </LinkButton>
80
+
81
+ )
82
+ }
83
+
84
+ export default GithubStarsButton
@@ -0,0 +1,25 @@
1
+ import parseGithubUrl from './index.js'
2
+
3
+ describe('<Subnav /> - parseGithubUrl', () => {
4
+ it('should gracefully handle an invalid URL', () => {
5
+ // Suppress console.warn for this test, we expect warnings
6
+ jest.spyOn(console, 'warn')
7
+ global.console.warn.mockImplementation(() => {})
8
+ expect(parseGithubUrl(undefined)).toBe(false)
9
+ expect(parseGithubUrl('blah-blah')).toBe(false)
10
+ // Restore console.warn for further tests
11
+ global.console.warn.mockRestore()
12
+ })
13
+
14
+ it('should handle a non-GitHub URL', () => {
15
+ expect(parseGithubUrl('https://www.example.com/dbosoft/maxback')).toBe(
16
+ false
17
+ )
18
+ })
19
+
20
+ it('should parse the org and repo from a GitHub URL', () => {
21
+ const testInput = 'https://www.github.com/dbosoft/maxback'
22
+ expect(parseGithubUrl(testInput).repo).toBe('maxback')
23
+ expect(parseGithubUrl(testInput).org).toBe('dbosoft')
24
+ })
25
+ })
@@ -0,0 +1,25 @@
1
+ /*
2
+ * Given:
3
+ * urlString (string) valid URL string
4
+ * Return:
5
+ * { org, repo }, where the url is ~= www.github.com/{org}/{repo}
6
+ * or false otherwise (eg if not a valid URL, or if not a GitHub url)
7
+ */
8
+ function parseGithubUrl(urlString: string) {
9
+ try {
10
+ const urlObj = new URL(urlString)
11
+ if (urlObj.hostname !== 'www.github.com') return false
12
+ const parts = urlObj.pathname.split('/').filter(Boolean)
13
+ const org = parts[0]
14
+ const repo = parts[1]
15
+ return { org, repo }
16
+ } catch (err) {
17
+ console.warn(
18
+ 'Warning! An invalid URL has probably been supplied to the GitHub ctaLink in <Subnav />. The corresponding error:'
19
+ )
20
+ console.warn(err)
21
+ return false
22
+ }
23
+ }
24
+
25
+ export default parseGithubUrl
@@ -0,0 +1,4 @@
1
+ <svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
2
+ <title>GitHub</title>
3
+ <path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12" />
4
+ </svg>
@@ -0,0 +1,46 @@
1
+ import classNames from 'classnames'
2
+ import LinkButton from '@dbosoft/react-uicore/linkbutton'
3
+ import Link from 'next/link'
4
+ import type { ICtaItem } from '../..'
5
+ import GithubStarsLink from './github-stars-link'
6
+
7
+ function CtaLinks(props: {
8
+ links: ICtaItem[],
9
+ hideGithubStars: boolean,
10
+ isInDropdown: boolean
11
+ }) {
12
+ const { links, hideGithubStars, isInDropdown } = props
13
+ return (
14
+ <div className={classNames("flex gap-2", isInDropdown ? "pt-4 flex-col" : "items-center ml-4 space-x-1 ")}>
15
+ {links.map((link, stableIdx) => {
16
+
17
+ const textKey = link.text.toLowerCase()
18
+ const isGithub = textKey === 'github'
19
+
20
+ if (isGithub)
21
+ return (
22
+ <GithubStarsLink
23
+
24
+ hideGithubStars={hideGithubStars}
25
+ key={stableIdx}
26
+ url={link.url}
27
+ />
28
+ )
29
+ return (
30
+ <LinkButton
31
+
32
+ Link={Link}
33
+ className={link.className}
34
+ key={stableIdx}
35
+ linktype={link.linktype}
36
+ text={link.text}
37
+ url={link.url}
38
+ variant={link.variant}
39
+ />
40
+ )
41
+ })}
42
+ </div>
43
+ )
44
+ }
45
+
46
+ export default CtaLinks
@@ -0,0 +1,63 @@
1
+ import type { ReactElement } from 'react';
2
+ import React from 'react'
3
+ import LinkWrap from '@dbosoft/react-uicore/link-wrap'
4
+ import NavItemText from '../nav-item-text'
5
+ import type { MenuItem } from '../..'
6
+ import Link from 'next/link';
7
+
8
+ function MenuItemsDefault(props: {
9
+ menuItems: MenuItem[]
10
+ }) {
11
+ const { menuItems } = props
12
+
13
+ return (
14
+ <ul
15
+ className="inline-flex items-center">
16
+ {/* <a
17
+ key={item.name}
18
+ href={item.href}
19
+ className={classNames(
20
+ item.current
21
+ ? 'border-indigo-500 text-gray-900'
22
+ : 'border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700',
23
+ 'inline-flex items-center px-1 pt-1 border-b-2 text-sm font-medium'
24
+ )}
25
+ aria-current={item.current ? 'page' : undefined}
26
+ ></a> */}
27
+ {menuItems.map((menuItem, stableIdx) => {
28
+ if (menuItem === 'divider') {
29
+ return <VerticalDivider key={stableIdx} />
30
+ }
31
+ const { text, url } = menuItem
32
+ return <NavLink
33
+ isActive={menuItem.active || false}
34
+ key={stableIdx}
35
+ text={text}
36
+ url={url}
37
+ />
38
+ })}
39
+ </ul>
40
+ )
41
+ }
42
+ function VerticalDivider() {
43
+ return (
44
+ <li>
45
+ <span className="bg-gray-400 w-0.5 h-6 mx-1 block" />
46
+ </li>
47
+ )
48
+ }
49
+
50
+
51
+ function NavLink(props: { text: string; url: string; isActive: boolean }) {
52
+ const { text, url, isActive } = props
53
+ return (
54
+ <li>
55
+ <LinkWrap Link={Link} className="px-2" href={url}>
56
+ <NavItemText isActive={isActive} text={text} />
57
+ </LinkWrap>
58
+ </li>
59
+ )
60
+ }
61
+
62
+
63
+ export default MenuItemsDefault