@dbosoft/nextjs-uicore 1.0.3 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +17 -0
- package/package.json +10 -6
- package/src/subnav/index.tsx +14 -28
- package/src/subnav/partials/CtaLinks/index.tsx +2 -2
- package/src/subnav/partials/MenuItemsDefault/index.tsx +1 -12
- package/src/subnav/partials/MenuItemsOverflow/index.tsx +3 -2
- package/src/subnav/partials/nav-item-text/index.tsx +2 -2
- package/src/tabs/Tabs.tsx +50 -0
- package/src/tabs/TabsClient.tsx +75 -0
- package/src/tabs/index.ts +3 -0
- package/src/tabs/server.ts +2 -0
- package/src/themeselector/index.tsx +119 -0
- package/src/tabs/hooks/use-scroll-left.ts +0 -27
- package/src/tabs/hooks/use-window-size.js +0 -33
- package/src/tabs/icons/chevron-right.svg +0 -1
- package/src/tabs/icons/tooltip.svg +0 -1
- package/src/tabs/index.tsx +0 -102
- package/src/tabs/partials/tab-trigger/index.tsx +0 -66
- package/src/tabs/partials/tab-trigger/style.module.scss +0 -69
- package/src/tabs/partials/tab-triggers/index.tsx +0 -241
- package/src/tabs/partials/tab-triggers/style.module.scss +0 -193
- package/src/tabs/partials/tooltip/index.tsx +0 -112
- package/src/tabs/partials/tooltip/style.module.scss +0 -38
- package/src/tabs/provider.js +0 -18
- package/src/tabs/style.module.css +0 -4
- package/src/tabs/utils/smooth-scroll.js +0 -88
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# @dbosoft/nextjs-uicore
|
|
2
2
|
|
|
3
|
+
## 1.2.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 676c725: Server side download pages
|
|
8
|
+
|
|
9
|
+
### Patch Changes
|
|
10
|
+
|
|
11
|
+
- Updated dependencies [676c725]
|
|
12
|
+
- @dbosoft/react-uicore@1.1.0
|
|
13
|
+
|
|
14
|
+
## 1.1.0
|
|
15
|
+
|
|
16
|
+
### Minor Changes
|
|
17
|
+
|
|
18
|
+
- 7a843e1: darkmode support for subnav
|
|
19
|
+
|
|
3
20
|
## 1.0.3
|
|
4
21
|
|
|
5
22
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -2,14 +2,16 @@
|
|
|
2
2
|
"name": "@dbosoft/nextjs-uicore",
|
|
3
3
|
"description": "Core UI components for Next.js",
|
|
4
4
|
"author": "dbosoft",
|
|
5
|
-
"version": "1.0
|
|
5
|
+
"version": "1.2.0",
|
|
6
6
|
"sideEffects": false,
|
|
7
7
|
"license": "MIT",
|
|
8
8
|
"exports": {
|
|
9
9
|
".": "./src",
|
|
10
10
|
"./head": "./src/head/index.tsx",
|
|
11
|
-
"./tabs": "./src/tabs/index.
|
|
12
|
-
"./
|
|
11
|
+
"./tabs": "./src/tabs/index.ts",
|
|
12
|
+
"./tabs/server": "./src/tabs/server.ts",
|
|
13
|
+
"./subnav": "./src/subnav/index.tsx",
|
|
14
|
+
"./themeselector": "./src/themeselector/index.tsx"
|
|
13
15
|
},
|
|
14
16
|
"devDependencies": {
|
|
15
17
|
"@types/react": "^18.2.46",
|
|
@@ -18,20 +20,22 @@
|
|
|
18
20
|
"next": "^14.0.4",
|
|
19
21
|
"react": "^18.2.0",
|
|
20
22
|
"typescript": "^5.3.3",
|
|
21
|
-
"@dbosoft/web-types": "1.0.0",
|
|
22
23
|
"@dbosoft/eslint-config": "1.0.0",
|
|
24
|
+
"@dbosoft/web-types": "1.0.0",
|
|
23
25
|
"@dbosoft/typescript-config": "1.0.0"
|
|
24
26
|
},
|
|
25
27
|
"publishConfig": {
|
|
26
28
|
"access": "public"
|
|
27
29
|
},
|
|
28
30
|
"dependencies": {
|
|
29
|
-
"@headlessui/react": "^
|
|
31
|
+
"@headlessui/react": "^2.2.1",
|
|
30
32
|
"@heroicons/react": "^2.1.3",
|
|
31
33
|
"classnames": "^2.3.1",
|
|
34
|
+
"clsx": "^2.1.0",
|
|
32
35
|
"isomorphic-unfetch": "^4.0.2",
|
|
36
|
+
"next-themes": "^0.4.3",
|
|
33
37
|
"unfetch": "^4.2.0",
|
|
34
|
-
"@dbosoft/react-uicore": "1.
|
|
38
|
+
"@dbosoft/react-uicore": "1.1.0"
|
|
35
39
|
},
|
|
36
40
|
"scripts": {
|
|
37
41
|
"lint": "eslint . --max-warnings 0",
|
package/src/subnav/index.tsx
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import { Disclosure } from '@headlessui/react'
|
|
1
|
+
import { Disclosure, DisclosureButton, DisclosurePanel } from '@headlessui/react'
|
|
2
2
|
import { Bars3Icon, XMarkIcon } from '@heroicons/react/24/outline'
|
|
3
|
-
import classNames from 'classnames'
|
|
4
3
|
import type { LinkType, Variant } from '@dbosoft/react-uicore/linkbutton'
|
|
5
4
|
import MenuItemsOverflow from './partials/MenuItemsOverflow'
|
|
6
5
|
import MenuItemsDefault from './partials/MenuItemsDefault'
|
|
@@ -8,6 +7,8 @@ import CtaLinks from './partials/CtaLinks'
|
|
|
8
7
|
import useStuckRef from './helpers/useStuckRef'
|
|
9
8
|
import TitleLink from './partials/TitleLink'
|
|
10
9
|
import style from './style.module.scss'
|
|
10
|
+
import clsx from 'clsx'
|
|
11
|
+
import { ThemeSelector } from '../themeselector'
|
|
11
12
|
|
|
12
13
|
export type MenuItem = 'divider' | ILinkMenuItem;
|
|
13
14
|
|
|
@@ -41,7 +42,7 @@ interface ISubNavProps {
|
|
|
41
42
|
titleLink?: ITitleLink,
|
|
42
43
|
titleContent?: JSX.Element,
|
|
43
44
|
ctaLinks: ICtaItem[],
|
|
44
|
-
hideGithubStars: boolean
|
|
45
|
+
hideGithubStars: boolean
|
|
45
46
|
menuItems: MenuItem[],
|
|
46
47
|
menuItemsAlign: 'left' | 'right',
|
|
47
48
|
className?: string
|
|
@@ -60,7 +61,7 @@ function Subnav({
|
|
|
60
61
|
const { isStuck, stuckRef } = useStuckRef([])
|
|
61
62
|
|
|
62
63
|
return (
|
|
63
|
-
<div className={
|
|
64
|
+
<div className={clsx(style.root, `bg-white dark:bg-slate-900`, className, {
|
|
64
65
|
[style.isSticky]: isStuck,
|
|
65
66
|
})} ref={stuckRef}>
|
|
66
67
|
<Disclosure as="nav" >
|
|
@@ -68,7 +69,7 @@ function Subnav({
|
|
|
68
69
|
<>
|
|
69
70
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
|
70
71
|
<div className="flex justify-between h-16">
|
|
71
|
-
<div className={
|
|
72
|
+
<div className={clsx(`flex grow`, menuItemsAlign == `right` ? ` justify-between` : ``)}>
|
|
72
73
|
|
|
73
74
|
<div className="flex-shrink-0 flex items-center ">
|
|
74
75
|
{titleLink &&<TitleLink
|
|
@@ -88,46 +89,31 @@ function Subnav({
|
|
|
88
89
|
/>
|
|
89
90
|
</div>
|
|
90
91
|
</div>
|
|
92
|
+
|
|
91
93
|
<div className="-mr-2 flex items-center sm:hidden ">
|
|
92
94
|
{/* Mobile menu button */}
|
|
93
|
-
<
|
|
95
|
+
<DisclosureButton 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">
|
|
94
96
|
<span className="sr-only">Open main menu</span>
|
|
95
97
|
{open ? (
|
|
96
98
|
<XMarkIcon aria-hidden="true" className="block h-6 w-6" />
|
|
97
99
|
) : (
|
|
98
100
|
<Bars3Icon aria-hidden="true" className="block h-6 w-6" />
|
|
99
101
|
)}
|
|
100
|
-
</
|
|
102
|
+
</DisclosureButton>
|
|
101
103
|
</div>
|
|
102
104
|
</div>
|
|
105
|
+
|
|
103
106
|
</div>
|
|
104
107
|
|
|
105
|
-
<
|
|
108
|
+
<DisclosurePanel className="sm:hidden">
|
|
106
109
|
<div className="pt-2 pb-3 space-y-1">
|
|
107
110
|
<MenuItemsOverflow
|
|
108
111
|
ctaLinks={ctaLinks}
|
|
109
112
|
hideGithubStars={hideGithubStars}
|
|
110
113
|
menuItems={menuItems}
|
|
111
|
-
/>
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
key={item.name}
|
|
115
|
-
as="a"
|
|
116
|
-
href={item.href}
|
|
117
|
-
className={classNames(
|
|
118
|
-
item.current
|
|
119
|
-
? 'bg-indigo-50 border-indigo-500 text-indigo-700'
|
|
120
|
-
: 'border-transparent text-gray-600 hover:bg-gray-50 hover:border-gray-300 hover:text-gray-800',
|
|
121
|
-
'block pl-3 pr-4 py-2 border-l-4 text-base font-medium'
|
|
122
|
-
)}
|
|
123
|
-
aria-current={item.current ? 'page' : undefined}
|
|
124
|
-
>
|
|
125
|
-
{item.name}
|
|
126
|
-
</Disclosure.Button>
|
|
127
|
-
))}
|
|
128
|
-
*/} </div>
|
|
129
|
-
|
|
130
|
-
</Disclosure.Panel>
|
|
114
|
+
/> </div>
|
|
115
|
+
|
|
116
|
+
</DisclosurePanel>
|
|
131
117
|
</>
|
|
132
118
|
)}
|
|
133
119
|
</Disclosure>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import clsx from 'clsx'
|
|
2
2
|
import LinkButton from '@dbosoft/react-uicore/linkbutton'
|
|
3
3
|
import Link from 'next/link'
|
|
4
4
|
import type { ICtaItem } from '../..'
|
|
@@ -11,7 +11,7 @@ function CtaLinks(props: {
|
|
|
11
11
|
}) {
|
|
12
12
|
const { links, hideGithubStars, isInDropdown } = props
|
|
13
13
|
return (
|
|
14
|
-
<div className={
|
|
14
|
+
<div className={clsx("flex gap-2", isInDropdown ? "pt-4 flex-col" : "items-center ml-4 space-x-1 ")}>
|
|
15
15
|
{links.map((link, stableIdx) => {
|
|
16
16
|
|
|
17
17
|
const textKey = link.text.toLowerCase()
|
|
@@ -13,17 +13,6 @@ function MenuItemsDefault(props: {
|
|
|
13
13
|
return (
|
|
14
14
|
<ul
|
|
15
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
16
|
{menuItems.map((menuItem, stableIdx) => {
|
|
28
17
|
if (menuItem === 'divider') {
|
|
29
18
|
return <VerticalDivider key={stableIdx} />
|
|
@@ -42,7 +31,7 @@ function MenuItemsDefault(props: {
|
|
|
42
31
|
function VerticalDivider() {
|
|
43
32
|
return (
|
|
44
33
|
<li>
|
|
45
|
-
<span className="bg-gray-400 w-0.5 h-6 mx-1 block" />
|
|
34
|
+
<span className="bg-gray-400 dark:bg-gray-200 w-0.5 h-6 mx-1 block" />
|
|
46
35
|
</li>
|
|
47
36
|
)
|
|
48
37
|
}
|
|
@@ -5,7 +5,7 @@ import CtaLinks from '../CtaLinks'
|
|
|
5
5
|
import type { ICtaItem, MenuItem } from '../..'
|
|
6
6
|
import s from './style.module.scss'
|
|
7
7
|
import Link from 'next/link';
|
|
8
|
-
|
|
8
|
+
import clsx from 'clsx'
|
|
9
9
|
|
|
10
10
|
interface MenuItemsOverflowProps {
|
|
11
11
|
menuItems: MenuItem[],
|
|
@@ -39,9 +39,10 @@ export default function MenuItemsOverflow({ menuItems, ctaLinks, hideGithubStars
|
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
function SubmenuItem({ url, text, active }: { url: string; text: string; active: boolean }) {
|
|
42
|
+
|
|
42
43
|
return (
|
|
43
44
|
<li>
|
|
44
|
-
<LinkWrap Link={Link} className={s.submenuItem} href={url} title={text}>
|
|
45
|
+
<LinkWrap Link={Link} className={clsx(s.submenuItem, "text-black dark:text-white" )} href={url} title={text}>
|
|
45
46
|
{text}
|
|
46
47
|
</LinkWrap>
|
|
47
48
|
</li>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import clsx from 'clsx'
|
|
2
2
|
import React from 'react'
|
|
3
3
|
|
|
4
4
|
/**
|
|
@@ -19,7 +19,7 @@ function NavItemText({
|
|
|
19
19
|
}): React.ReactElement {
|
|
20
20
|
return (
|
|
21
21
|
<span
|
|
22
|
-
className={
|
|
22
|
+
className={clsx("text-black dark:text-white text-sm font-semibold", isActive ? "border-b-2 pb-2 border-black" : "")}
|
|
23
23
|
>
|
|
24
24
|
{text}
|
|
25
25
|
</span>
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { TabList, Tab, TabPanels, TabPanel, TabGroup } from "@headlessui/react"
|
|
2
|
+
import { FC, Suspense } from "react"
|
|
3
|
+
|
|
4
|
+
import TabsClient, { TabConfig, TabLink } from "./TabsClient"
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
export const TabsContent: FC<{
|
|
8
|
+
tabs: TabConfig[],
|
|
9
|
+
tabNames: string[]
|
|
10
|
+
}> = ({
|
|
11
|
+
tabs,
|
|
12
|
+
tabNames
|
|
13
|
+
}) => {
|
|
14
|
+
|
|
15
|
+
return <>
|
|
16
|
+
<TabList className={` pt-2 flex justify-center`}>
|
|
17
|
+
{tabs.map((tab, index) => <Suspense key={tab.name}><Tab as={TabLink} className={tab.className} index={index} tabs={tabNames}>{tab.displayName}</Tab></Suspense>)}
|
|
18
|
+
</TabList>
|
|
19
|
+
<TabPanels className={`mt-4 pt-0 border-t-2 border-brand-light dark:border-brand-dark`}>
|
|
20
|
+
{tabs.map((tab, index) => <TabPanel className={`py-4`} key={index}>{tab.content}</TabPanel>)}
|
|
21
|
+
</TabPanels>
|
|
22
|
+
</>
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
const Tabs: FC<{
|
|
27
|
+
tabs: TabConfig[],
|
|
28
|
+
currentTab: string | undefined
|
|
29
|
+
}> = ({
|
|
30
|
+
tabs,
|
|
31
|
+
currentTab
|
|
32
|
+
}) => {
|
|
33
|
+
let selectedTab = currentTab ? tabs.findIndex(tab => tab.name === currentTab) : 0;
|
|
34
|
+
if (selectedTab == -1)
|
|
35
|
+
selectedTab = 0;
|
|
36
|
+
|
|
37
|
+
const tabNames = tabs.map(tab => tab.name);
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
return <div className="pb-8"><Suspense fallback={<TabGroup selectedIndex={selectedTab}>
|
|
41
|
+
<TabsContent tabs={tabs} tabNames={tabNames} /></TabGroup>}>
|
|
42
|
+
<TabsClient initialTab={selectedTab} tabs={tabNames}>
|
|
43
|
+
<TabsContent tabs={tabs} tabNames={tabNames} />
|
|
44
|
+
</TabsClient>
|
|
45
|
+
</Suspense>
|
|
46
|
+
</div>
|
|
47
|
+
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export default Tabs;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
|
4
|
+
import { FC, ReactNode, forwardRef, useState } from "react";
|
|
5
|
+
import { TabGroup } from "@headlessui/react"
|
|
6
|
+
import Link from "next/link";
|
|
7
|
+
import clsx from "clsx";
|
|
8
|
+
|
|
9
|
+
export type TabConfig = {
|
|
10
|
+
name: string,
|
|
11
|
+
displayName: string
|
|
12
|
+
className?: string
|
|
13
|
+
content: ReactNode
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const createNavUrl = (index: number, tabs: string[], pathname: string, searchParams: URLSearchParams) => {
|
|
17
|
+
const lastSegment = pathname.split(`/`).pop();
|
|
18
|
+
let newPath = pathname;
|
|
19
|
+
|
|
20
|
+
if (lastSegment && tabs.includes(lastSegment)) {
|
|
21
|
+
newPath = pathname.substring(0, pathname.lastIndexOf(`/`));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const newTab = index > 0 ? tabs[index] : ``;
|
|
25
|
+
newPath = newPath + `/` + newTab;
|
|
26
|
+
|
|
27
|
+
const query = searchParams.toString();
|
|
28
|
+
|
|
29
|
+
if (query)
|
|
30
|
+
return (newPath + `?` + query);
|
|
31
|
+
else
|
|
32
|
+
return newPath;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export const TabLink = forwardRef<HTMLAnchorElement, React.PropsWithChildren<{
|
|
36
|
+
index: number, tabs: string[], className: string
|
|
37
|
+
}>>((props, ref) => {
|
|
38
|
+
|
|
39
|
+
const pathname = usePathname()
|
|
40
|
+
const searchParams = useSearchParams()
|
|
41
|
+
|
|
42
|
+
return <Link prefetch={false} role="tab" {...props} className={clsx(`data-[selected]:border-secondary data-[selected]:text-link-light dark:data-[selected]:text-sky-300`,
|
|
43
|
+
`border-transparent text-content-secondary hover:border-secondary hover:text-sky-600 dark:hover:text-sky-200 `,
|
|
44
|
+
`whitespace-nowrap border-b-2 mx-3 px-2 py-2 text-base font-semibold`)} ref={ref} href={createNavUrl(props.index, props.tabs, pathname, searchParams)}
|
|
45
|
+
>
|
|
46
|
+
{props.children}
|
|
47
|
+
</Link >
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
TabLink.displayName = `TabLink`;
|
|
51
|
+
|
|
52
|
+
const TabsClient: FC<{
|
|
53
|
+
children: ReactNode,
|
|
54
|
+
initialTab: number,
|
|
55
|
+
tabs: string[]
|
|
56
|
+
}> = ({
|
|
57
|
+
children,
|
|
58
|
+
tabs,
|
|
59
|
+
initialTab
|
|
60
|
+
}) => {
|
|
61
|
+
const router = useRouter();
|
|
62
|
+
const pathname = usePathname();
|
|
63
|
+
const searchParams = useSearchParams();
|
|
64
|
+
const [selectedTab, setSelectedTab] = useState(initialTab);
|
|
65
|
+
|
|
66
|
+
return <TabGroup className="text-content-primary" manual selectedIndex={selectedTab}
|
|
67
|
+
defaultIndex={initialTab} onChange={(index) => {
|
|
68
|
+
router.push(createNavUrl(index, tabs, pathname, searchParams));
|
|
69
|
+
setSelectedTab(index);
|
|
70
|
+
}}>
|
|
71
|
+
{children}
|
|
72
|
+
</TabGroup>
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export default TabsClient
|
|
@@ -0,0 +1,119 @@
|
|
|
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
|
+
const themes = [
|
|
7
|
+
{ name: 'Light', value: 'light', icon: LightIcon },
|
|
8
|
+
{ name: 'Dark', value: 'dark', icon: DarkIcon },
|
|
9
|
+
{ name: 'System', value: 'system', icon: SystemIcon },
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
function LightIcon(props: React.ComponentPropsWithoutRef<'svg'>) {
|
|
13
|
+
return (
|
|
14
|
+
<svg aria-hidden="true" viewBox="0 0 16 16" {...props}>
|
|
15
|
+
<path
|
|
16
|
+
fillRule="evenodd"
|
|
17
|
+
clipRule="evenodd"
|
|
18
|
+
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"
|
|
19
|
+
/>
|
|
20
|
+
</svg>
|
|
21
|
+
)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function DarkIcon(props: React.ComponentPropsWithoutRef<'svg'>) {
|
|
25
|
+
return (
|
|
26
|
+
<svg aria-hidden="true" viewBox="0 0 16 16" {...props}>
|
|
27
|
+
<path
|
|
28
|
+
fillRule="evenodd"
|
|
29
|
+
clipRule="evenodd"
|
|
30
|
+
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"
|
|
31
|
+
/>
|
|
32
|
+
</svg>
|
|
33
|
+
)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function SystemIcon(props: React.ComponentPropsWithoutRef<'svg'>) {
|
|
37
|
+
return (
|
|
38
|
+
<svg aria-hidden="true" viewBox="0 0 16 16" {...props}>
|
|
39
|
+
<path
|
|
40
|
+
fillRule="evenodd"
|
|
41
|
+
clipRule="evenodd"
|
|
42
|
+
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"
|
|
43
|
+
/>
|
|
44
|
+
</svg>
|
|
45
|
+
)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function ThemeSelector(
|
|
49
|
+
props: React.ComponentPropsWithoutRef<typeof Listbox<'div'>>,
|
|
50
|
+
) {
|
|
51
|
+
let { theme, setTheme } = useTheme()
|
|
52
|
+
let [mounted, setMounted] = useState(false)
|
|
53
|
+
|
|
54
|
+
useEffect(() => {
|
|
55
|
+
setMounted(true)
|
|
56
|
+
}, [])
|
|
57
|
+
|
|
58
|
+
if (!mounted) {
|
|
59
|
+
return <div className="h-6 w-6" />
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return (
|
|
63
|
+
<Listbox as="div" value={theme} onChange={setTheme} {...props}>
|
|
64
|
+
<ListboxLabel className="sr-only">Theme</ListboxLabel>
|
|
65
|
+
<ListboxButton
|
|
66
|
+
className="flex h-9 w-12 mt-3 items-center justify-center rounded-lg shadow-md shadow-black/5 ring-1 ring-black/5 dark:bg-slate-700 dark:ring-inset dark:ring-white/5"
|
|
67
|
+
aria-label="Theme"
|
|
68
|
+
>
|
|
69
|
+
<LightIcon
|
|
70
|
+
className={clsx(
|
|
71
|
+
'h-4 w-4 dark:hidden',
|
|
72
|
+
theme === 'system' ? 'fill-slate-400' : 'fill-sky-400',
|
|
73
|
+
)}
|
|
74
|
+
/>
|
|
75
|
+
<DarkIcon
|
|
76
|
+
className={clsx(
|
|
77
|
+
'hidden h-4 w-4 dark:block',
|
|
78
|
+
theme === 'system' ? 'fill-slate-400' : 'fill-sky-400',
|
|
79
|
+
)}
|
|
80
|
+
/>
|
|
81
|
+
</ListboxButton>
|
|
82
|
+
<ListboxOptions className="absolute left-1/2 top-full mt-3 w-36 -translate-x-1/2 space-y-1 rounded-xl bg-white p-3 text-sm font-medium shadow-md shadow-black/5 ring-1 ring-black/5 dark:bg-slate-800 dark:ring-white/5">
|
|
83
|
+
{themes.map((theme) => (
|
|
84
|
+
<ListboxOption
|
|
85
|
+
key={theme.value}
|
|
86
|
+
value={theme.value}
|
|
87
|
+
className={({ focus, selected }) =>
|
|
88
|
+
clsx(
|
|
89
|
+
'flex cursor-pointer select-none items-center rounded-[0.625rem] p-1',
|
|
90
|
+
{
|
|
91
|
+
'text-sky-500': selected,
|
|
92
|
+
'text-slate-900 dark:text-white': focus && !selected,
|
|
93
|
+
'text-slate-700 dark:text-slate-400': !focus && !selected,
|
|
94
|
+
'bg-slate-100 dark:bg-slate-900/40': focus,
|
|
95
|
+
},
|
|
96
|
+
)
|
|
97
|
+
}
|
|
98
|
+
>
|
|
99
|
+
{({ selected }) => (
|
|
100
|
+
<>
|
|
101
|
+
<div className="rounded-md bg-white p-1 shadow ring-1 ring-slate-900/5 dark:bg-slate-700 dark:ring-inset dark:ring-white/5">
|
|
102
|
+
<theme.icon
|
|
103
|
+
className={clsx(
|
|
104
|
+
'h-4 w-4',
|
|
105
|
+
selected
|
|
106
|
+
? 'fill-sky-400 dark:fill-sky-400'
|
|
107
|
+
: 'fill-slate-400',
|
|
108
|
+
)}
|
|
109
|
+
/>
|
|
110
|
+
</div>
|
|
111
|
+
<div className="ml-3">{theme.name}</div>
|
|
112
|
+
</>
|
|
113
|
+
)}
|
|
114
|
+
</ListboxOption>
|
|
115
|
+
))}
|
|
116
|
+
</ListboxOptions>
|
|
117
|
+
</Listbox>
|
|
118
|
+
)
|
|
119
|
+
}
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
import { useState, useEffect, useRef, MutableRefObject } from 'react'
|
|
2
|
-
|
|
3
|
-
export default function useScrollLeft(): [
|
|
4
|
-
scrollRef: MutableRefObject<$TSFixMe>,
|
|
5
|
-
scrollLeft: number
|
|
6
|
-
] {
|
|
7
|
-
const scrollRef = useRef(null)
|
|
8
|
-
const [scrollLeft, setScrollLeft] = useState()
|
|
9
|
-
|
|
10
|
-
useEffect(() => {
|
|
11
|
-
const scrollElem = scrollRef.current as $TSFixMe;
|
|
12
|
-
// Handler to call when scrollLeft may be affected
|
|
13
|
-
function handleScroll() {
|
|
14
|
-
if (!scrollRef.current) return null
|
|
15
|
-
// Set scroll data to state
|
|
16
|
-
setScrollLeft((scrollRef.current as $TSFixMe).scrollLeft)
|
|
17
|
-
}
|
|
18
|
-
// Add event listener
|
|
19
|
-
scrollElem.addEventListener('scroll', handleScroll)
|
|
20
|
-
// Call handler right away so state gets updated with initial scroll position
|
|
21
|
-
handleScroll()
|
|
22
|
-
// Remove event listener on cleanup
|
|
23
|
-
return () => scrollElem.removeEventListener('scroll', handleScroll)
|
|
24
|
-
}, [scrollRef])
|
|
25
|
-
|
|
26
|
-
return [scrollRef, 0]
|
|
27
|
-
}
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
import { useState, useEffect } from 'react'
|
|
2
|
-
|
|
3
|
-
// https://usehooks.com/useWindowSize/
|
|
4
|
-
export default function useWindowSize() {
|
|
5
|
-
// Initialize state with undefined width/height so server and client renders match
|
|
6
|
-
// Learn more here: https://joshwcomeau.com/react/the-perils-of-rehydration/
|
|
7
|
-
const [windowSize, setWindowSize] = useState({
|
|
8
|
-
width: undefined,
|
|
9
|
-
height: undefined,
|
|
10
|
-
})
|
|
11
|
-
|
|
12
|
-
useEffect(() => {
|
|
13
|
-
// Handler to call on window resize
|
|
14
|
-
function handleResize() {
|
|
15
|
-
// Set window width/height to state
|
|
16
|
-
setWindowSize({
|
|
17
|
-
width: window.innerWidth,
|
|
18
|
-
height: window.innerHeight,
|
|
19
|
-
})
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
// Add event listener
|
|
23
|
-
window.addEventListener('resize', handleResize)
|
|
24
|
-
|
|
25
|
-
// Call handler right away so state gets updated with initial window size
|
|
26
|
-
handleResize()
|
|
27
|
-
|
|
28
|
-
// Remove event listener on cleanup
|
|
29
|
-
return () => window.removeEventListener('resize', handleResize)
|
|
30
|
-
}, []) // Empty array ensures that effect is only run on mount
|
|
31
|
-
|
|
32
|
-
return windowSize
|
|
33
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M9 18l6-6-6-6" stroke="#000" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M9.09 9a3 3 0 015.83 1c0 2-3 3-3 3" stroke="var(--gray-4)" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path clip-rule="evenodd" d="M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2 2 6.477 2 12s4.477 10 10 10z" stroke="var(--gray-4)" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M12 17.387a.379.379 0 100-.758.379.379 0 000 .758z" fill="var(--gray-4)" stroke="var(--gray-4)" stroke-width="1.5" stroke-linejoin="round"/></svg>
|
package/src/tabs/index.tsx
DELETED
|
@@ -1,102 +0,0 @@
|
|
|
1
|
-
import React, { useState, useEffect } from 'react'
|
|
2
|
-
import { TabTriggerType } from './partials/tab-trigger'
|
|
3
|
-
import TabTriggers from './partials/tab-triggers'
|
|
4
|
-
import TabProvider, { useTabGroups } from './provider'
|
|
5
|
-
import s from './style.module.css'
|
|
6
|
-
|
|
7
|
-
interface TabChildProps {
|
|
8
|
-
/** Renders the tab contents. */
|
|
9
|
-
children: React.ReactElement
|
|
10
|
-
/** Plain text used for the tab heading. */
|
|
11
|
-
heading: string
|
|
12
|
-
/** Accepts a string such that, when the tab is active, other Tab elements outside the instance with a matching `group` value will automatically be shown. Note that `TabProvider` is required in order for this feature to function.v */
|
|
13
|
-
group?: string
|
|
14
|
-
/** Plain text displayed in a tooltip beside the tab heading. */
|
|
15
|
-
tooltip?: string
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
function Tab({ children }: TabChildProps): React.ReactElement {
|
|
19
|
-
return <>{children}</>
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
interface TabsProps {
|
|
23
|
-
/** Children to be displayed as tabs. Each child accepts the props `{ children: ReactElement, heading: string, tooltip?: string, group?: string }` */
|
|
24
|
-
children: Array<React.ReactElement<TabChildProps>>
|
|
25
|
-
/** If set to true, the tabs are centered in their container. Default is left-aligned. */
|
|
26
|
-
centered?: boolean
|
|
27
|
-
/** Optional className to add to the root element. */
|
|
28
|
-
className?: string
|
|
29
|
-
/** Set the default tab by its index. If not set, or if out of range, will default to the first tab, at index 0. */
|
|
30
|
-
defaultTabIdx?: number
|
|
31
|
-
/** If set to true, the bottom border underneath the tabs will fill the available width. Default border fills the constrained `.g-grid-container`. */
|
|
32
|
-
fullWidthBorder?: boolean
|
|
33
|
-
/** Optional callback which is executed when a new tab is selected. */
|
|
34
|
-
onChange?: (targetTabIdx: number, targetTabGroup?: string) => void
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function Tabs({
|
|
38
|
-
children,
|
|
39
|
-
className,
|
|
40
|
-
defaultTabIdx = 0,
|
|
41
|
-
centered = false,
|
|
42
|
-
fullWidthBorder = false,
|
|
43
|
-
onChange,
|
|
44
|
-
}: TabsProps): React.ReactElement {
|
|
45
|
-
// Ensures a single child object converts to an array
|
|
46
|
-
children = Array.prototype.concat(children)
|
|
47
|
-
|
|
48
|
-
const isDefaultOutOfBounds =
|
|
49
|
-
defaultTabIdx >= children.length || defaultTabIdx < 0
|
|
50
|
-
|
|
51
|
-
const [activeTabIdx, setActiveTabIdx] = useState(
|
|
52
|
-
// if specified default is out of bounds (ie, it's determined at runtime),
|
|
53
|
-
// fallback to 0 to avoid throwing an error
|
|
54
|
-
isDefaultOutOfBounds ? 0 : defaultTabIdx
|
|
55
|
-
)
|
|
56
|
-
const groupCtx = useTabGroups()
|
|
57
|
-
|
|
58
|
-
function setActiveTab(targetIdx : number, groupId?:string) {
|
|
59
|
-
setActiveTabIdx(targetIdx)
|
|
60
|
-
if (onChange) onChange(targetIdx, groupId)
|
|
61
|
-
if (groupCtx) groupCtx.setActiveTabGroup(groupId)
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
useEffect(() => {
|
|
65
|
-
const hasGroups = children.filter((tab) => tab.props.group).length > 0
|
|
66
|
-
if (
|
|
67
|
-
process.env.NODE_ENV !== 'production' &&
|
|
68
|
-
hasGroups &&
|
|
69
|
-
groupCtx === undefined
|
|
70
|
-
) {
|
|
71
|
-
console.warn(
|
|
72
|
-
'@dbosoft/react-tabs: The `TabProvider` cannot be accessed. Make sure it wraps the `Tabs` components so Tab Groups can work properly.'
|
|
73
|
-
)
|
|
74
|
-
}
|
|
75
|
-
}, [children, groupCtx])
|
|
76
|
-
|
|
77
|
-
if (!children) {
|
|
78
|
-
process.env.NODE_ENV !== 'production' &&
|
|
79
|
-
console.warn(
|
|
80
|
-
'@dbosoft/react-tabs: There are no `Tab` children for the `Tabs` component to render.'
|
|
81
|
-
)
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
return (
|
|
85
|
-
<section className={className}>
|
|
86
|
-
<TabTriggers
|
|
87
|
-
tabs={children.map((tab, index) => {
|
|
88
|
-
const { heading, group, tooltip } = tab.props
|
|
89
|
-
return { index, heading, group, tooltip } as TabTriggerType
|
|
90
|
-
})}
|
|
91
|
-
centered={centered}
|
|
92
|
-
fullWidthBorder={fullWidthBorder}
|
|
93
|
-
activeTabIdx={activeTabIdx}
|
|
94
|
-
setActiveTab={setActiveTab}
|
|
95
|
-
/>
|
|
96
|
-
<div className={s.content}>{children[activeTabIdx].props.children}</div>
|
|
97
|
-
</section>
|
|
98
|
-
)
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
export default Tabs
|
|
102
|
-
export { TabProvider, useTabGroups, Tab }
|