@dbosoft/nextjs-uicore 1.6.0 → 1.6.2
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/CHANGELOG.md +14 -0
- package/eslint.config.mjs +3 -3
- package/package.json +6 -2
- package/src/subnav/helpers/useStuckRef.ts +50 -50
- package/src/subnav/index.tsx +134 -134
- package/src/subnav/partials/CtaLinks/github-stars-link/formatStarCount/index.test.js +25 -25
- package/src/subnav/partials/CtaLinks/github-stars-link/parseGithubUrl/index.test.js +25 -25
- package/src/subnav/partials/CtaLinks/icons/github.svg +3 -3
- package/src/subnav/partials/MenuItemsDefault/style.module.scss +71 -71
- package/src/subnav/partials/MenuItemsOverflow/style.module.scss +51 -51
- package/src/subnav/partials/TitleLink/index.tsx +25 -25
- package/src/subnav/partials/nav-item-text/style.module.scss +19 -19
- package/src/subnav/style.module.scss +13 -13
- package/src/themeselector/index.tsx +139 -139
- package/tsconfig.json +7 -7
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# @dbosoft/nextjs-uicore
|
|
2
2
|
|
|
3
|
+
## 1.6.2
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Updated dependencies [e1e8cbf]
|
|
8
|
+
- @dbosoft/react-uicore@1.4.2
|
|
9
|
+
|
|
10
|
+
## 1.6.1
|
|
11
|
+
|
|
12
|
+
### Patch Changes
|
|
13
|
+
|
|
14
|
+
- Updated dependencies [909ea3a]
|
|
15
|
+
- @dbosoft/react-uicore@1.4.1
|
|
16
|
+
|
|
3
17
|
## 1.6.0
|
|
4
18
|
|
|
5
19
|
### Minor Changes
|
package/eslint.config.mjs
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import react from '@dbosoft/eslint-config/react'
|
|
2
|
-
|
|
3
|
-
export default [...react]
|
|
1
|
+
import react from '@dbosoft/eslint-config/react'
|
|
2
|
+
|
|
3
|
+
export default [...react]
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"description": "DEPRECATED: Use @dbosoft/react-uicore/tabs for pure React tabs and @dbosoft/nextjs-site-core/ui/tabs for Next.js integration. All other components (head, subnav, themeselector) are replaced by @dbosoft/nextjs-site-core.",
|
|
4
4
|
"deprecated": true,
|
|
5
5
|
"author": "dbosoft",
|
|
6
|
-
"version": "1.6.
|
|
6
|
+
"version": "1.6.2",
|
|
7
7
|
"sideEffects": false,
|
|
8
8
|
"license": "MIT",
|
|
9
9
|
"exports": {
|
|
@@ -30,12 +30,16 @@
|
|
|
30
30
|
"publishConfig": {
|
|
31
31
|
"access": "public"
|
|
32
32
|
},
|
|
33
|
+
"repository": {
|
|
34
|
+
"type": "git",
|
|
35
|
+
"url": "git+https://github.com/dbosoft/react-components.git"
|
|
36
|
+
},
|
|
33
37
|
"dependencies": {
|
|
34
38
|
"@headlessui/react": ">=2.2.0",
|
|
35
39
|
"@heroicons/react": ">=2.1.0",
|
|
36
40
|
"clsx": ">=2.1.0",
|
|
37
41
|
"next-themes": ">=0.4.3",
|
|
38
|
-
"@dbosoft/react-uicore": "1.4.
|
|
42
|
+
"@dbosoft/react-uicore": "1.4.2"
|
|
39
43
|
},
|
|
40
44
|
"scripts": {
|
|
41
45
|
"check-types": "tsc --noEmit",
|
|
@@ -1,50 +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
|
-
}
|
|
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
|
+
}
|
package/src/subnav/index.tsx
CHANGED
|
@@ -1,134 +1,134 @@
|
|
|
1
|
-
import { Disclosure, DisclosureButton, DisclosurePanel } from '@headlessui/react'
|
|
2
|
-
import { Bars3Icon, XMarkIcon } from '@heroicons/react/24/outline'
|
|
3
|
-
import type { LinkType, Variant } from '@dbosoft/react-uicore/linkbutton'
|
|
4
|
-
import MenuItemsOverflow from './partials/MenuItemsOverflow'
|
|
5
|
-
import MenuItemsDefault from './partials/MenuItemsDefault'
|
|
6
|
-
import CtaLinks from './partials/CtaLinks'
|
|
7
|
-
import useStuckRef from './helpers/useStuckRef'
|
|
8
|
-
import TitleLink from './partials/TitleLink'
|
|
9
|
-
import style from './style.module.scss'
|
|
10
|
-
import clsx from 'clsx'
|
|
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
|
-
export interface ITitleLink {
|
|
27
|
-
url: string,
|
|
28
|
-
text?: string,
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
export interface ICtaItem {
|
|
33
|
-
text: string,
|
|
34
|
-
url: string,
|
|
35
|
-
variant?: Variant
|
|
36
|
-
linktype?: LinkType
|
|
37
|
-
className?: string,
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export interface SubNavLabels {
|
|
41
|
-
openMainMenu: string
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
const defaultSubNavLabels: SubNavLabels = {
|
|
45
|
-
openMainMenu: 'Open main menu',
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
interface ISubNavProps {
|
|
49
|
-
titleLink?: ITitleLink,
|
|
50
|
-
titleContent?: React.JSX.Element,
|
|
51
|
-
ctaLinks: ICtaItem[],
|
|
52
|
-
hideGithubStars: boolean
|
|
53
|
-
menuItems: MenuItem[],
|
|
54
|
-
menuItemsAlign: 'left' | 'right',
|
|
55
|
-
className?: string,
|
|
56
|
-
labels?: Partial<SubNavLabels>
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function Subnav({
|
|
60
|
-
className,
|
|
61
|
-
titleLink,
|
|
62
|
-
ctaLinks = [],
|
|
63
|
-
hideGithubStars,
|
|
64
|
-
menuItems,
|
|
65
|
-
menuItemsAlign = `right`,
|
|
66
|
-
titleContent,
|
|
67
|
-
labels: labelsProp
|
|
68
|
-
}: ISubNavProps) {
|
|
69
|
-
const labels = { ...defaultSubNavLabels, ...labelsProp }
|
|
70
|
-
|
|
71
|
-
const { isStuck, stuckRef } = useStuckRef([])
|
|
72
|
-
|
|
73
|
-
return (
|
|
74
|
-
<div className={clsx(style.root, `bg-white dark:bg-slate-900`, className, {
|
|
75
|
-
[style.isSticky]: isStuck,
|
|
76
|
-
})} ref={stuckRef}>
|
|
77
|
-
<Disclosure as="nav" >
|
|
78
|
-
{({ open }) => (
|
|
79
|
-
<>
|
|
80
|
-
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
|
81
|
-
<div className="flex justify-between h-16">
|
|
82
|
-
<div className={clsx(`flex grow`, menuItemsAlign == `right` ? ` justify-between` : ``)}>
|
|
83
|
-
|
|
84
|
-
<div className="flex-shrink-0 flex items-center ">
|
|
85
|
-
{titleLink &&<TitleLink
|
|
86
|
-
text={titleLink.text}
|
|
87
|
-
|
|
88
|
-
url={titleLink.url} >{titleContent}</TitleLink>}
|
|
89
|
-
{!titleLink && titleContent}
|
|
90
|
-
</div>
|
|
91
|
-
<div className="hidden sm:-my-px sm:ml-6 sm:flex">
|
|
92
|
-
<MenuItemsDefault
|
|
93
|
-
menuItems={menuItems}
|
|
94
|
-
/>
|
|
95
|
-
<CtaLinks
|
|
96
|
-
hideGithubStars={hideGithubStars}
|
|
97
|
-
isInDropdown={false}
|
|
98
|
-
links={ctaLinks}
|
|
99
|
-
/>
|
|
100
|
-
</div>
|
|
101
|
-
</div>
|
|
102
|
-
|
|
103
|
-
<div className="-mr-2 flex items-center sm:hidden ">
|
|
104
|
-
{/* Mobile menu button */}
|
|
105
|
-
<DisclosureButton className="bg-white inline-flex items-center justify-center p-2 rounded-control text-gray-400 hover:text-gray-500 hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-secondary">
|
|
106
|
-
<span className="sr-only">{labels.openMainMenu}</span>
|
|
107
|
-
{open ? (
|
|
108
|
-
<XMarkIcon aria-hidden="true" className="block h-6 w-6" />
|
|
109
|
-
) : (
|
|
110
|
-
<Bars3Icon aria-hidden="true" className="block h-6 w-6" />
|
|
111
|
-
)}
|
|
112
|
-
</DisclosureButton>
|
|
113
|
-
</div>
|
|
114
|
-
</div>
|
|
115
|
-
|
|
116
|
-
</div>
|
|
117
|
-
|
|
118
|
-
<DisclosurePanel className="sm:hidden">
|
|
119
|
-
<div className="pt-2 pb-3 space-y-1">
|
|
120
|
-
<MenuItemsOverflow
|
|
121
|
-
ctaLinks={ctaLinks}
|
|
122
|
-
hideGithubStars={hideGithubStars}
|
|
123
|
-
menuItems={menuItems}
|
|
124
|
-
/> </div>
|
|
125
|
-
|
|
126
|
-
</DisclosurePanel>
|
|
127
|
-
</>
|
|
128
|
-
)}
|
|
129
|
-
</Disclosure>
|
|
130
|
-
</div>
|
|
131
|
-
)
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
export default Subnav
|
|
1
|
+
import { Disclosure, DisclosureButton, DisclosurePanel } from '@headlessui/react'
|
|
2
|
+
import { Bars3Icon, XMarkIcon } from '@heroicons/react/24/outline'
|
|
3
|
+
import type { LinkType, Variant } from '@dbosoft/react-uicore/linkbutton'
|
|
4
|
+
import MenuItemsOverflow from './partials/MenuItemsOverflow'
|
|
5
|
+
import MenuItemsDefault from './partials/MenuItemsDefault'
|
|
6
|
+
import CtaLinks from './partials/CtaLinks'
|
|
7
|
+
import useStuckRef from './helpers/useStuckRef'
|
|
8
|
+
import TitleLink from './partials/TitleLink'
|
|
9
|
+
import style from './style.module.scss'
|
|
10
|
+
import clsx from 'clsx'
|
|
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
|
+
export interface ITitleLink {
|
|
27
|
+
url: string,
|
|
28
|
+
text?: string,
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
export interface ICtaItem {
|
|
33
|
+
text: string,
|
|
34
|
+
url: string,
|
|
35
|
+
variant?: Variant
|
|
36
|
+
linktype?: LinkType
|
|
37
|
+
className?: string,
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface SubNavLabels {
|
|
41
|
+
openMainMenu: string
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const defaultSubNavLabels: SubNavLabels = {
|
|
45
|
+
openMainMenu: 'Open main menu',
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
interface ISubNavProps {
|
|
49
|
+
titleLink?: ITitleLink,
|
|
50
|
+
titleContent?: React.JSX.Element,
|
|
51
|
+
ctaLinks: ICtaItem[],
|
|
52
|
+
hideGithubStars: boolean
|
|
53
|
+
menuItems: MenuItem[],
|
|
54
|
+
menuItemsAlign: 'left' | 'right',
|
|
55
|
+
className?: string,
|
|
56
|
+
labels?: Partial<SubNavLabels>
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function Subnav({
|
|
60
|
+
className,
|
|
61
|
+
titleLink,
|
|
62
|
+
ctaLinks = [],
|
|
63
|
+
hideGithubStars,
|
|
64
|
+
menuItems,
|
|
65
|
+
menuItemsAlign = `right`,
|
|
66
|
+
titleContent,
|
|
67
|
+
labels: labelsProp
|
|
68
|
+
}: ISubNavProps) {
|
|
69
|
+
const labels = { ...defaultSubNavLabels, ...labelsProp }
|
|
70
|
+
|
|
71
|
+
const { isStuck, stuckRef } = useStuckRef([])
|
|
72
|
+
|
|
73
|
+
return (
|
|
74
|
+
<div className={clsx(style.root, `bg-white dark:bg-slate-900`, className, {
|
|
75
|
+
[style.isSticky]: isStuck,
|
|
76
|
+
})} ref={stuckRef}>
|
|
77
|
+
<Disclosure as="nav" >
|
|
78
|
+
{({ open }) => (
|
|
79
|
+
<>
|
|
80
|
+
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
|
81
|
+
<div className="flex justify-between h-16">
|
|
82
|
+
<div className={clsx(`flex grow`, menuItemsAlign == `right` ? ` justify-between` : ``)}>
|
|
83
|
+
|
|
84
|
+
<div className="flex-shrink-0 flex items-center ">
|
|
85
|
+
{titleLink &&<TitleLink
|
|
86
|
+
text={titleLink.text}
|
|
87
|
+
|
|
88
|
+
url={titleLink.url} >{titleContent}</TitleLink>}
|
|
89
|
+
{!titleLink && titleContent}
|
|
90
|
+
</div>
|
|
91
|
+
<div className="hidden sm:-my-px sm:ml-6 sm:flex">
|
|
92
|
+
<MenuItemsDefault
|
|
93
|
+
menuItems={menuItems}
|
|
94
|
+
/>
|
|
95
|
+
<CtaLinks
|
|
96
|
+
hideGithubStars={hideGithubStars}
|
|
97
|
+
isInDropdown={false}
|
|
98
|
+
links={ctaLinks}
|
|
99
|
+
/>
|
|
100
|
+
</div>
|
|
101
|
+
</div>
|
|
102
|
+
|
|
103
|
+
<div className="-mr-2 flex items-center sm:hidden ">
|
|
104
|
+
{/* Mobile menu button */}
|
|
105
|
+
<DisclosureButton className="bg-white inline-flex items-center justify-center p-2 rounded-control text-gray-400 hover:text-gray-500 hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-secondary">
|
|
106
|
+
<span className="sr-only">{labels.openMainMenu}</span>
|
|
107
|
+
{open ? (
|
|
108
|
+
<XMarkIcon aria-hidden="true" className="block h-6 w-6" />
|
|
109
|
+
) : (
|
|
110
|
+
<Bars3Icon aria-hidden="true" className="block h-6 w-6" />
|
|
111
|
+
)}
|
|
112
|
+
</DisclosureButton>
|
|
113
|
+
</div>
|
|
114
|
+
</div>
|
|
115
|
+
|
|
116
|
+
</div>
|
|
117
|
+
|
|
118
|
+
<DisclosurePanel className="sm:hidden">
|
|
119
|
+
<div className="pt-2 pb-3 space-y-1">
|
|
120
|
+
<MenuItemsOverflow
|
|
121
|
+
ctaLinks={ctaLinks}
|
|
122
|
+
hideGithubStars={hideGithubStars}
|
|
123
|
+
menuItems={menuItems}
|
|
124
|
+
/> </div>
|
|
125
|
+
|
|
126
|
+
</DisclosurePanel>
|
|
127
|
+
</>
|
|
128
|
+
)}
|
|
129
|
+
</Disclosure>
|
|
130
|
+
</div>
|
|
131
|
+
)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export default Subnav
|
|
@@ -1,25 +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
|
-
})
|
|
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
|
+
})
|
|
@@ -1,25 +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
|
-
})
|
|
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
|
+
})
|
|
@@ -1,4 +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" />
|
|
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
4
|
</svg>
|
|
@@ -1,71 +1,71 @@
|
|
|
1
|
-
.root {
|
|
2
|
-
display: flex;
|
|
3
|
-
padding: 0 32px;
|
|
4
|
-
margin: 0 auto;
|
|
5
|
-
align-items: center;
|
|
6
|
-
list-style: none;
|
|
7
|
-
|
|
8
|
-
&.alignRight {
|
|
9
|
-
margin-right: 0;
|
|
10
|
-
padding-right: 16px;
|
|
11
|
-
}
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
.listItem {
|
|
15
|
-
position: relative;
|
|
16
|
-
white-space: nowrap;
|
|
17
|
-
margin: 0;
|
|
18
|
-
padding: 0;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
.navLink {
|
|
22
|
-
composes: g-type-body-small-strong from global;
|
|
23
|
-
position: relative;
|
|
24
|
-
padding: 0 16px;
|
|
25
|
-
line-height: 2.5rem;
|
|
26
|
-
display: flex;
|
|
27
|
-
align-items: center;
|
|
28
|
-
white-space: nowrap;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
.submenuItem {
|
|
32
|
-
composes: g-type-body-small-strong from global;
|
|
33
|
-
color: var(--black);
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
.submenuModal {
|
|
37
|
-
border-radius: 4px;
|
|
38
|
-
box-shadow: 0 8px 12px rgba(37, 38, 45, 0.08);
|
|
39
|
-
background: var(--white);
|
|
40
|
-
z-index: 1;
|
|
41
|
-
display: block;
|
|
42
|
-
position: absolute;
|
|
43
|
-
top: 100%;
|
|
44
|
-
margin: 8px 0 0 0;
|
|
45
|
-
padding: 24px;
|
|
46
|
-
left: 50%;
|
|
47
|
-
transform: translateX(-50%);
|
|
48
|
-
list-style: none;
|
|
49
|
-
|
|
50
|
-
& li {
|
|
51
|
-
& a:hover .text {
|
|
52
|
-
text-decoration: underline;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
& + li {
|
|
56
|
-
margin-top: 6px;
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
&.isCollapsed {
|
|
61
|
-
display: none;
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
.verticalDivider {
|
|
66
|
-
background: var(--gray-5);
|
|
67
|
-
height: 1.75rem;
|
|
68
|
-
width: 1px;
|
|
69
|
-
margin: 0 8px;
|
|
70
|
-
display: block;
|
|
71
|
-
}
|
|
1
|
+
.root {
|
|
2
|
+
display: flex;
|
|
3
|
+
padding: 0 32px;
|
|
4
|
+
margin: 0 auto;
|
|
5
|
+
align-items: center;
|
|
6
|
+
list-style: none;
|
|
7
|
+
|
|
8
|
+
&.alignRight {
|
|
9
|
+
margin-right: 0;
|
|
10
|
+
padding-right: 16px;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
.listItem {
|
|
15
|
+
position: relative;
|
|
16
|
+
white-space: nowrap;
|
|
17
|
+
margin: 0;
|
|
18
|
+
padding: 0;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.navLink {
|
|
22
|
+
composes: g-type-body-small-strong from global;
|
|
23
|
+
position: relative;
|
|
24
|
+
padding: 0 16px;
|
|
25
|
+
line-height: 2.5rem;
|
|
26
|
+
display: flex;
|
|
27
|
+
align-items: center;
|
|
28
|
+
white-space: nowrap;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.submenuItem {
|
|
32
|
+
composes: g-type-body-small-strong from global;
|
|
33
|
+
color: var(--black);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.submenuModal {
|
|
37
|
+
border-radius: 4px;
|
|
38
|
+
box-shadow: 0 8px 12px rgba(37, 38, 45, 0.08);
|
|
39
|
+
background: var(--white);
|
|
40
|
+
z-index: 1;
|
|
41
|
+
display: block;
|
|
42
|
+
position: absolute;
|
|
43
|
+
top: 100%;
|
|
44
|
+
margin: 8px 0 0 0;
|
|
45
|
+
padding: 24px;
|
|
46
|
+
left: 50%;
|
|
47
|
+
transform: translateX(-50%);
|
|
48
|
+
list-style: none;
|
|
49
|
+
|
|
50
|
+
& li {
|
|
51
|
+
& a:hover .text {
|
|
52
|
+
text-decoration: underline;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
& + li {
|
|
56
|
+
margin-top: 6px;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
&.isCollapsed {
|
|
61
|
+
display: none;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.verticalDivider {
|
|
66
|
+
background: var(--gray-5);
|
|
67
|
+
height: 1.75rem;
|
|
68
|
+
width: 1px;
|
|
69
|
+
margin: 0 8px;
|
|
70
|
+
display: block;
|
|
71
|
+
}
|
|
@@ -1,51 +1,51 @@
|
|
|
1
|
-
.root {
|
|
2
|
-
position: relative;
|
|
3
|
-
margin-left: auto;
|
|
4
|
-
}
|
|
5
|
-
|
|
6
|
-
.dropdown {
|
|
7
|
-
background: var(--white);
|
|
8
|
-
border-radius: 4px;
|
|
9
|
-
box-shadow: 0 8px 12px rgba(37, 38, 45, 0.08);
|
|
10
|
-
margin-top: 8px;
|
|
11
|
-
max-height: calc(100vh - 300%);
|
|
12
|
-
overflow-y: auto;
|
|
13
|
-
padding: 12px 24px 0 24px;
|
|
14
|
-
position: absolute;
|
|
15
|
-
right: 0;
|
|
16
|
-
top: 100%;
|
|
17
|
-
width: 256px;
|
|
18
|
-
z-index: 1;
|
|
19
|
-
|
|
20
|
-
&.isCollapsed {
|
|
21
|
-
display: none;
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
.ulElem {
|
|
26
|
-
padding: 0;
|
|
27
|
-
margin: 0;
|
|
28
|
-
list-style: none;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
.submenuTitle {
|
|
32
|
-
composes: g-type-label from global;
|
|
33
|
-
margin: 12px 0 8px 0;
|
|
34
|
-
color: var(--gray-3);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
.divider {
|
|
38
|
-
margin: 12px 0;
|
|
39
|
-
border: 0;
|
|
40
|
-
padding: 0;
|
|
41
|
-
height: 1px;
|
|
42
|
-
background: var(--gray-5);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
.submenuItem {
|
|
46
|
-
composes: g-type-body-small-strong from global;
|
|
47
|
-
display: block;
|
|
48
|
-
padding: 4px 0;
|
|
49
|
-
line-height: 1.6em;
|
|
50
|
-
color: var(--black);
|
|
51
|
-
}
|
|
1
|
+
.root {
|
|
2
|
+
position: relative;
|
|
3
|
+
margin-left: auto;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
.dropdown {
|
|
7
|
+
background: var(--white);
|
|
8
|
+
border-radius: 4px;
|
|
9
|
+
box-shadow: 0 8px 12px rgba(37, 38, 45, 0.08);
|
|
10
|
+
margin-top: 8px;
|
|
11
|
+
max-height: calc(100vh - 300%);
|
|
12
|
+
overflow-y: auto;
|
|
13
|
+
padding: 12px 24px 0 24px;
|
|
14
|
+
position: absolute;
|
|
15
|
+
right: 0;
|
|
16
|
+
top: 100%;
|
|
17
|
+
width: 256px;
|
|
18
|
+
z-index: 1;
|
|
19
|
+
|
|
20
|
+
&.isCollapsed {
|
|
21
|
+
display: none;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
.ulElem {
|
|
26
|
+
padding: 0;
|
|
27
|
+
margin: 0;
|
|
28
|
+
list-style: none;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.submenuTitle {
|
|
32
|
+
composes: g-type-label from global;
|
|
33
|
+
margin: 12px 0 8px 0;
|
|
34
|
+
color: var(--gray-3);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.divider {
|
|
38
|
+
margin: 12px 0;
|
|
39
|
+
border: 0;
|
|
40
|
+
padding: 0;
|
|
41
|
+
height: 1px;
|
|
42
|
+
background: var(--gray-5);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.submenuItem {
|
|
46
|
+
composes: g-type-body-small-strong from global;
|
|
47
|
+
display: block;
|
|
48
|
+
padding: 4px 0;
|
|
49
|
+
line-height: 1.6em;
|
|
50
|
+
color: var(--black);
|
|
51
|
+
}
|
|
@@ -1,25 +1,25 @@
|
|
|
1
|
-
import type { FC, ReactElement, ReactNode } from 'react'
|
|
2
|
-
import LinkWrap from '@dbosoft/react-uicore/link-wrap'
|
|
3
|
-
import Link from 'next/link'
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
const TitleLink: FC<{
|
|
7
|
-
text?: string,
|
|
8
|
-
url: string,
|
|
9
|
-
children?: ReactNode
|
|
10
|
-
}> = ({
|
|
11
|
-
text,
|
|
12
|
-
url, children }) => {
|
|
13
|
-
return (
|
|
14
|
-
<LinkWrap
|
|
15
|
-
Link={Link}
|
|
16
|
-
className="font-semibold text-gray-900 tracking-widest text-lg inline"
|
|
17
|
-
href={url}
|
|
18
|
-
title={text}
|
|
19
|
-
>{text}
|
|
20
|
-
{children}
|
|
21
|
-
</LinkWrap>
|
|
22
|
-
)
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export default TitleLink
|
|
1
|
+
import type { FC, ReactElement, ReactNode } from 'react'
|
|
2
|
+
import LinkWrap from '@dbosoft/react-uicore/link-wrap'
|
|
3
|
+
import Link from 'next/link'
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
const TitleLink: FC<{
|
|
7
|
+
text?: string,
|
|
8
|
+
url: string,
|
|
9
|
+
children?: ReactNode
|
|
10
|
+
}> = ({
|
|
11
|
+
text,
|
|
12
|
+
url, children }) => {
|
|
13
|
+
return (
|
|
14
|
+
<LinkWrap
|
|
15
|
+
Link={Link}
|
|
16
|
+
className="font-semibold text-gray-900 tracking-widest text-lg inline"
|
|
17
|
+
href={url}
|
|
18
|
+
title={text}
|
|
19
|
+
>{text}
|
|
20
|
+
{children}
|
|
21
|
+
</LinkWrap>
|
|
22
|
+
)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export default TitleLink
|
|
@@ -1,19 +1,19 @@
|
|
|
1
|
-
.root {
|
|
2
|
-
color: var(--black);
|
|
3
|
-
position: relative;
|
|
4
|
-
|
|
5
|
-
&::after {
|
|
6
|
-
content: '';
|
|
7
|
-
position: absolute;
|
|
8
|
-
left: 0;
|
|
9
|
-
right: 0;
|
|
10
|
-
bottom: 0;
|
|
11
|
-
height: 2px;
|
|
12
|
-
background: var(--black);
|
|
13
|
-
opacity: 0;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
&.isActive::after {
|
|
17
|
-
opacity: 1;
|
|
18
|
-
}
|
|
19
|
-
}
|
|
1
|
+
.root {
|
|
2
|
+
color: var(--black);
|
|
3
|
+
position: relative;
|
|
4
|
+
|
|
5
|
+
&::after {
|
|
6
|
+
content: '';
|
|
7
|
+
position: absolute;
|
|
8
|
+
left: 0;
|
|
9
|
+
right: 0;
|
|
10
|
+
bottom: 0;
|
|
11
|
+
height: 2px;
|
|
12
|
+
background: var(--black);
|
|
13
|
+
opacity: 0;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
&.isActive::after {
|
|
17
|
+
opacity: 1;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
.root {
|
|
2
|
-
--gray-6-transparent: rgba(174, 176, 183, 0.45);
|
|
3
|
-
position: sticky;
|
|
4
|
-
top: -1px;
|
|
5
|
-
z-index: 800;
|
|
6
|
-
border-top: 1px solid var(--gray-6-transparent);
|
|
7
|
-
border-bottom: 1px solid transparent;
|
|
8
|
-
transition: border-bottom-color 0.8s;
|
|
9
|
-
|
|
10
|
-
&.isSticky {
|
|
11
|
-
border-bottom-color: var(--gray-6-transparent);
|
|
12
|
-
}
|
|
13
|
-
}
|
|
1
|
+
.root {
|
|
2
|
+
--gray-6-transparent: rgba(174, 176, 183, 0.45);
|
|
3
|
+
position: sticky;
|
|
4
|
+
top: -1px;
|
|
5
|
+
z-index: 800;
|
|
6
|
+
border-top: 1px solid var(--gray-6-transparent);
|
|
7
|
+
border-bottom: 1px solid transparent;
|
|
8
|
+
transition: border-bottom-color 0.8s;
|
|
9
|
+
|
|
10
|
+
&.isSticky {
|
|
11
|
+
border-bottom-color: var(--gray-6-transparent);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -1,139 +1,139 @@
|
|
|
1
|
-
import { useEffect, useState } from 'react'
|
|
2
|
-
import { useTheme } from 'next-themes'
|
|
3
|
-
import { Listbox, ListboxButton, ListboxLabel, ListboxOption, ListboxOptions } from '@headlessui/react'
|
|
4
|
-
import clsx from 'clsx'
|
|
5
|
-
|
|
6
|
-
export interface ThemeSelectorLabels {
|
|
7
|
-
light: string
|
|
8
|
-
dark: string
|
|
9
|
-
system: string
|
|
10
|
-
theme: string
|
|
11
|
-
themeAriaLabel: string
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
const defaultThemeSelectorLabels: ThemeSelectorLabels = {
|
|
15
|
-
light: 'Light',
|
|
16
|
-
dark: 'Dark',
|
|
17
|
-
system: 'System',
|
|
18
|
-
theme: 'Theme',
|
|
19
|
-
themeAriaLabel: 'Theme',
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
function LightIcon(props: React.ComponentPropsWithoutRef<'svg'>) {
|
|
23
|
-
return (
|
|
24
|
-
<svg aria-hidden="true" viewBox="0 0 16 16" {...props}>
|
|
25
|
-
<path
|
|
26
|
-
fillRule="evenodd"
|
|
27
|
-
clipRule="evenodd"
|
|
28
|
-
d="M7 1a1 1 0 0 1 2 0v1a1 1 0 1 1-2 0V1Zm4 7a3 3 0 1 1-6 0 3 3 0 0 1 6 0Zm2.657-5.657a1 1 0 0 0-1.414 0l-.707.707a1 1 0 0 0 1.414 1.414l.707-.707a1 1 0 0 0 0-1.414Zm-1.415 11.313-.707-.707a1 1 0 0 1 1.415-1.415l.707.708a1 1 0 0 1-1.415 1.414ZM16 7.999a1 1 0 0 0-1-1h-1a1 1 0 1 0 0 2h1a1 1 0 0 0 1-1ZM7 14a1 1 0 1 1 2 0v1a1 1 0 1 1-2 0v-1Zm-2.536-2.464a1 1 0 0 0-1.414 0l-.707.707a1 1 0 0 0 1.414 1.414l.707-.707a1 1 0 0 0 0-1.414Zm0-8.486A1 1 0 0 1 3.05 4.464l-.707-.707a1 1 0 0 1 1.414-1.414l.707.707ZM3 8a1 1 0 0 0-1-1H1a1 1 0 0 0 0 2h1a1 1 0 0 0 1-1Z"
|
|
29
|
-
/>
|
|
30
|
-
</svg>
|
|
31
|
-
)
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
function DarkIcon(props: React.ComponentPropsWithoutRef<'svg'>) {
|
|
35
|
-
return (
|
|
36
|
-
<svg aria-hidden="true" viewBox="0 0 16 16" {...props}>
|
|
37
|
-
<path
|
|
38
|
-
fillRule="evenodd"
|
|
39
|
-
clipRule="evenodd"
|
|
40
|
-
d="M7.23 3.333C7.757 2.905 7.68 2 7 2a6 6 0 1 0 0 12c.68 0 .758-.905.23-1.332A5.989 5.989 0 0 1 5 8c0-1.885.87-3.568 2.23-4.668ZM12 5a1 1 0 0 1 1 1 1 1 0 0 0 1 1 1 1 0 1 1 0 2 1 1 0 0 0-1 1 1 1 0 1 1-2 0 1 1 0 0 0-1-1 1 1 0 1 1 0-2 1 1 0 0 0 1-1 1 1 0 0 1 1-1Z"
|
|
41
|
-
/>
|
|
42
|
-
</svg>
|
|
43
|
-
)
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function SystemIcon(props: React.ComponentPropsWithoutRef<'svg'>) {
|
|
47
|
-
return (
|
|
48
|
-
<svg aria-hidden="true" viewBox="0 0 16 16" {...props}>
|
|
49
|
-
<path
|
|
50
|
-
fillRule="evenodd"
|
|
51
|
-
clipRule="evenodd"
|
|
52
|
-
d="M1 4a3 3 0 0 1 3-3h8a3 3 0 0 1 3 3v4a3 3 0 0 1-3 3h-1.5l.31 1.242c.084.333.36.573.63.808.091.08.182.158.264.24A1 1 0 0 1 11 15H5a1 1 0 0 1-.704-1.71c.082-.082.173-.16.264-.24.27-.235.546-.475.63-.808L5.5 11H4a3 3 0 0 1-3-3V4Zm3-1a1 1 0 0 0-1 1v4a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V4a1 1 0 0 0-1-1H4Z"
|
|
53
|
-
/>
|
|
54
|
-
</svg>
|
|
55
|
-
)
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
export function ThemeSelector({
|
|
59
|
-
labels: labelsProp,
|
|
60
|
-
...props
|
|
61
|
-
}: React.ComponentPropsWithoutRef<typeof Listbox<'div'>> & {
|
|
62
|
-
labels?: Partial<ThemeSelectorLabels>
|
|
63
|
-
}) {
|
|
64
|
-
const labels = { ...defaultThemeSelectorLabels, ...labelsProp }
|
|
65
|
-
const themes = [
|
|
66
|
-
{ name: labels.light, value: 'light', icon: LightIcon },
|
|
67
|
-
{ name: labels.dark, value: 'dark', icon: DarkIcon },
|
|
68
|
-
{ name: labels.system, value: 'system', icon: SystemIcon },
|
|
69
|
-
]
|
|
70
|
-
|
|
71
|
-
let { theme, setTheme } = useTheme()
|
|
72
|
-
let [mounted, setMounted] = useState(false)
|
|
73
|
-
|
|
74
|
-
useEffect(() => {
|
|
75
|
-
setMounted(true)
|
|
76
|
-
}, [])
|
|
77
|
-
|
|
78
|
-
if (!mounted) {
|
|
79
|
-
return <div className="h-6 w-6" />
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
return (
|
|
83
|
-
<Listbox as="div" value={theme} onChange={setTheme} {...props}>
|
|
84
|
-
<ListboxLabel className="sr-only">{labels.theme}</ListboxLabel>
|
|
85
|
-
<ListboxButton
|
|
86
|
-
className="flex h-9 w-12 mt-3 items-center justify-center rounded-container-sm shadow-popover shadow-black/5 ring-1 ring-black/5 dark:bg-slate-700 dark:ring-inset dark:ring-white/5"
|
|
87
|
-
aria-label={labels.themeAriaLabel}
|
|
88
|
-
>
|
|
89
|
-
<LightIcon
|
|
90
|
-
className={clsx(
|
|
91
|
-
'h-4 w-4 dark:hidden',
|
|
92
|
-
theme === 'system' ? 'fill-slate-400' : 'fill-sky-400',
|
|
93
|
-
)}
|
|
94
|
-
/>
|
|
95
|
-
<DarkIcon
|
|
96
|
-
className={clsx(
|
|
97
|
-
'hidden h-4 w-4 dark:block',
|
|
98
|
-
theme === 'system' ? 'fill-slate-400' : 'fill-sky-400',
|
|
99
|
-
)}
|
|
100
|
-
/>
|
|
101
|
-
</ListboxButton>
|
|
102
|
-
<ListboxOptions className="absolute left-1/2 top-full mt-3 w-36 -translate-x-1/2 space-y-1 rounded-container bg-white p-3 text-sm font-medium shadow-popover shadow-black/5 ring-1 ring-black/5 dark:bg-slate-800 dark:ring-white/5">
|
|
103
|
-
{themes.map((theme) => (
|
|
104
|
-
<ListboxOption
|
|
105
|
-
key={theme.value}
|
|
106
|
-
value={theme.value}
|
|
107
|
-
className={({ focus, selected }) =>
|
|
108
|
-
clsx(
|
|
109
|
-
'flex cursor-pointer select-none items-center rounded-[0.625rem] p-1',
|
|
110
|
-
{
|
|
111
|
-
'text-sky-500': selected,
|
|
112
|
-
'text-slate-900 dark:text-white': focus && !selected,
|
|
113
|
-
'text-slate-700 dark:text-slate-400': !focus && !selected,
|
|
114
|
-
'bg-slate-100 dark:bg-slate-900/40': focus,
|
|
115
|
-
},
|
|
116
|
-
)
|
|
117
|
-
}
|
|
118
|
-
>
|
|
119
|
-
{({ selected }) => (
|
|
120
|
-
<>
|
|
121
|
-
<div className="rounded-control bg-white p-1 shadow-control ring-1 ring-slate-900/5 dark:bg-slate-700 dark:ring-inset dark:ring-white/5">
|
|
122
|
-
<theme.icon
|
|
123
|
-
className={clsx(
|
|
124
|
-
'h-4 w-4',
|
|
125
|
-
selected
|
|
126
|
-
? 'fill-sky-400 dark:fill-sky-400'
|
|
127
|
-
: 'fill-slate-400',
|
|
128
|
-
)}
|
|
129
|
-
/>
|
|
130
|
-
</div>
|
|
131
|
-
<div className="ml-3">{theme.name}</div>
|
|
132
|
-
</>
|
|
133
|
-
)}
|
|
134
|
-
</ListboxOption>
|
|
135
|
-
))}
|
|
136
|
-
</ListboxOptions>
|
|
137
|
-
</Listbox>
|
|
138
|
-
)
|
|
139
|
-
}
|
|
1
|
+
import { useEffect, useState } from 'react'
|
|
2
|
+
import { useTheme } from 'next-themes'
|
|
3
|
+
import { Listbox, ListboxButton, ListboxLabel, ListboxOption, ListboxOptions } from '@headlessui/react'
|
|
4
|
+
import clsx from 'clsx'
|
|
5
|
+
|
|
6
|
+
export interface ThemeSelectorLabels {
|
|
7
|
+
light: string
|
|
8
|
+
dark: string
|
|
9
|
+
system: string
|
|
10
|
+
theme: string
|
|
11
|
+
themeAriaLabel: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const defaultThemeSelectorLabels: ThemeSelectorLabels = {
|
|
15
|
+
light: 'Light',
|
|
16
|
+
dark: 'Dark',
|
|
17
|
+
system: 'System',
|
|
18
|
+
theme: 'Theme',
|
|
19
|
+
themeAriaLabel: 'Theme',
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function LightIcon(props: React.ComponentPropsWithoutRef<'svg'>) {
|
|
23
|
+
return (
|
|
24
|
+
<svg aria-hidden="true" viewBox="0 0 16 16" {...props}>
|
|
25
|
+
<path
|
|
26
|
+
fillRule="evenodd"
|
|
27
|
+
clipRule="evenodd"
|
|
28
|
+
d="M7 1a1 1 0 0 1 2 0v1a1 1 0 1 1-2 0V1Zm4 7a3 3 0 1 1-6 0 3 3 0 0 1 6 0Zm2.657-5.657a1 1 0 0 0-1.414 0l-.707.707a1 1 0 0 0 1.414 1.414l.707-.707a1 1 0 0 0 0-1.414Zm-1.415 11.313-.707-.707a1 1 0 0 1 1.415-1.415l.707.708a1 1 0 0 1-1.415 1.414ZM16 7.999a1 1 0 0 0-1-1h-1a1 1 0 1 0 0 2h1a1 1 0 0 0 1-1ZM7 14a1 1 0 1 1 2 0v1a1 1 0 1 1-2 0v-1Zm-2.536-2.464a1 1 0 0 0-1.414 0l-.707.707a1 1 0 0 0 1.414 1.414l.707-.707a1 1 0 0 0 0-1.414Zm0-8.486A1 1 0 0 1 3.05 4.464l-.707-.707a1 1 0 0 1 1.414-1.414l.707.707ZM3 8a1 1 0 0 0-1-1H1a1 1 0 0 0 0 2h1a1 1 0 0 0 1-1Z"
|
|
29
|
+
/>
|
|
30
|
+
</svg>
|
|
31
|
+
)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function DarkIcon(props: React.ComponentPropsWithoutRef<'svg'>) {
|
|
35
|
+
return (
|
|
36
|
+
<svg aria-hidden="true" viewBox="0 0 16 16" {...props}>
|
|
37
|
+
<path
|
|
38
|
+
fillRule="evenodd"
|
|
39
|
+
clipRule="evenodd"
|
|
40
|
+
d="M7.23 3.333C7.757 2.905 7.68 2 7 2a6 6 0 1 0 0 12c.68 0 .758-.905.23-1.332A5.989 5.989 0 0 1 5 8c0-1.885.87-3.568 2.23-4.668ZM12 5a1 1 0 0 1 1 1 1 1 0 0 0 1 1 1 1 0 1 1 0 2 1 1 0 0 0-1 1 1 1 0 1 1-2 0 1 1 0 0 0-1-1 1 1 0 1 1 0-2 1 1 0 0 0 1-1 1 1 0 0 1 1-1Z"
|
|
41
|
+
/>
|
|
42
|
+
</svg>
|
|
43
|
+
)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function SystemIcon(props: React.ComponentPropsWithoutRef<'svg'>) {
|
|
47
|
+
return (
|
|
48
|
+
<svg aria-hidden="true" viewBox="0 0 16 16" {...props}>
|
|
49
|
+
<path
|
|
50
|
+
fillRule="evenodd"
|
|
51
|
+
clipRule="evenodd"
|
|
52
|
+
d="M1 4a3 3 0 0 1 3-3h8a3 3 0 0 1 3 3v4a3 3 0 0 1-3 3h-1.5l.31 1.242c.084.333.36.573.63.808.091.08.182.158.264.24A1 1 0 0 1 11 15H5a1 1 0 0 1-.704-1.71c.082-.082.173-.16.264-.24.27-.235.546-.475.63-.808L5.5 11H4a3 3 0 0 1-3-3V4Zm3-1a1 1 0 0 0-1 1v4a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V4a1 1 0 0 0-1-1H4Z"
|
|
53
|
+
/>
|
|
54
|
+
</svg>
|
|
55
|
+
)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function ThemeSelector({
|
|
59
|
+
labels: labelsProp,
|
|
60
|
+
...props
|
|
61
|
+
}: React.ComponentPropsWithoutRef<typeof Listbox<'div'>> & {
|
|
62
|
+
labels?: Partial<ThemeSelectorLabels>
|
|
63
|
+
}) {
|
|
64
|
+
const labels = { ...defaultThemeSelectorLabels, ...labelsProp }
|
|
65
|
+
const themes = [
|
|
66
|
+
{ name: labels.light, value: 'light', icon: LightIcon },
|
|
67
|
+
{ name: labels.dark, value: 'dark', icon: DarkIcon },
|
|
68
|
+
{ name: labels.system, value: 'system', icon: SystemIcon },
|
|
69
|
+
]
|
|
70
|
+
|
|
71
|
+
let { theme, setTheme } = useTheme()
|
|
72
|
+
let [mounted, setMounted] = useState(false)
|
|
73
|
+
|
|
74
|
+
useEffect(() => {
|
|
75
|
+
setMounted(true)
|
|
76
|
+
}, [])
|
|
77
|
+
|
|
78
|
+
if (!mounted) {
|
|
79
|
+
return <div className="h-6 w-6" />
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return (
|
|
83
|
+
<Listbox as="div" value={theme} onChange={setTheme} {...props}>
|
|
84
|
+
<ListboxLabel className="sr-only">{labels.theme}</ListboxLabel>
|
|
85
|
+
<ListboxButton
|
|
86
|
+
className="flex h-9 w-12 mt-3 items-center justify-center rounded-container-sm shadow-popover shadow-black/5 ring-1 ring-black/5 dark:bg-slate-700 dark:ring-inset dark:ring-white/5"
|
|
87
|
+
aria-label={labels.themeAriaLabel}
|
|
88
|
+
>
|
|
89
|
+
<LightIcon
|
|
90
|
+
className={clsx(
|
|
91
|
+
'h-4 w-4 dark:hidden',
|
|
92
|
+
theme === 'system' ? 'fill-slate-400' : 'fill-sky-400',
|
|
93
|
+
)}
|
|
94
|
+
/>
|
|
95
|
+
<DarkIcon
|
|
96
|
+
className={clsx(
|
|
97
|
+
'hidden h-4 w-4 dark:block',
|
|
98
|
+
theme === 'system' ? 'fill-slate-400' : 'fill-sky-400',
|
|
99
|
+
)}
|
|
100
|
+
/>
|
|
101
|
+
</ListboxButton>
|
|
102
|
+
<ListboxOptions className="absolute left-1/2 top-full mt-3 w-36 -translate-x-1/2 space-y-1 rounded-container bg-white p-3 text-sm font-medium shadow-popover shadow-black/5 ring-1 ring-black/5 dark:bg-slate-800 dark:ring-white/5">
|
|
103
|
+
{themes.map((theme) => (
|
|
104
|
+
<ListboxOption
|
|
105
|
+
key={theme.value}
|
|
106
|
+
value={theme.value}
|
|
107
|
+
className={({ focus, selected }) =>
|
|
108
|
+
clsx(
|
|
109
|
+
'flex cursor-pointer select-none items-center rounded-[0.625rem] p-1',
|
|
110
|
+
{
|
|
111
|
+
'text-sky-500': selected,
|
|
112
|
+
'text-slate-900 dark:text-white': focus && !selected,
|
|
113
|
+
'text-slate-700 dark:text-slate-400': !focus && !selected,
|
|
114
|
+
'bg-slate-100 dark:bg-slate-900/40': focus,
|
|
115
|
+
},
|
|
116
|
+
)
|
|
117
|
+
}
|
|
118
|
+
>
|
|
119
|
+
{({ selected }) => (
|
|
120
|
+
<>
|
|
121
|
+
<div className="rounded-control bg-white p-1 shadow-control ring-1 ring-slate-900/5 dark:bg-slate-700 dark:ring-inset dark:ring-white/5">
|
|
122
|
+
<theme.icon
|
|
123
|
+
className={clsx(
|
|
124
|
+
'h-4 w-4',
|
|
125
|
+
selected
|
|
126
|
+
? 'fill-sky-400 dark:fill-sky-400'
|
|
127
|
+
: 'fill-slate-400',
|
|
128
|
+
)}
|
|
129
|
+
/>
|
|
130
|
+
</div>
|
|
131
|
+
<div className="ml-3">{theme.name}</div>
|
|
132
|
+
</>
|
|
133
|
+
)}
|
|
134
|
+
</ListboxOption>
|
|
135
|
+
))}
|
|
136
|
+
</ListboxOptions>
|
|
137
|
+
</Listbox>
|
|
138
|
+
)
|
|
139
|
+
}
|
package/tsconfig.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
{
|
|
2
|
-
"extends": "@dbosoft/typescript-config/nextjs.json",
|
|
3
|
-
"compilerOptions": {
|
|
4
|
-
"outDir": "dist"
|
|
5
|
-
},
|
|
6
|
-
"include": ["src"],
|
|
7
|
-
"exclude": ["node_modules", "dist"]
|
|
1
|
+
{
|
|
2
|
+
"extends": "@dbosoft/typescript-config/nextjs.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"outDir": "dist"
|
|
5
|
+
},
|
|
6
|
+
"include": ["src"],
|
|
7
|
+
"exclude": ["node_modules", "dist"]
|
|
8
8
|
}
|