@dbosoft/nextjs-uicore 1.5.1 → 1.6.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 +11 -0
- package/package.json +7 -6
- package/src/head/index.tsx +119 -119
- package/src/subnav/helpers/useStuckRef.ts +1 -1
- package/src/subnav/index.tsx +1 -2
- package/src/subnav/partials/CtaLinks/github-stars-link/formatStarCount/index.ts +17 -17
- package/src/subnav/partials/CtaLinks/github-stars-link/index.tsx +96 -96
- package/src/subnav/partials/CtaLinks/github-stars-link/parseGithubUrl/index.ts +25 -25
- package/src/subnav/partials/CtaLinks/index.tsx +46 -46
- package/src/subnav/partials/MenuItemsDefault/index.tsx +52 -52
- package/src/subnav/partials/MenuItemsOverflow/index.tsx +51 -51
- package/src/subnav/partials/TitleLink/index.tsx +1 -1
- package/src/subnav/partials/nav-item-text/index.tsx +29 -29
- package/src/tabs/Tabs.tsx +50 -50
- package/src/tabs/TabsClient.tsx +75 -75
- package/src/tabs/index.ts +3 -3
- package/src/tabs/server.ts +2 -2
- package/src/themeselector/index.tsx +3 -3
- package/src/translations.ts +25 -25
- package/.turbo/turbo-build.log +0 -4
- package/.turbo/turbo-check-types.log +0 -4
|
@@ -1,46 +1,46 @@
|
|
|
1
|
-
import clsx from 'clsx'
|
|
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={clsx("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
|
|
1
|
+
import clsx from 'clsx'
|
|
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={clsx("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
|
|
@@ -1,52 +1,52 @@
|
|
|
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
|
-
{menuItems.map((menuItem, stableIdx) => {
|
|
17
|
-
if (menuItem === 'divider') {
|
|
18
|
-
return <VerticalDivider key={stableIdx} />
|
|
19
|
-
}
|
|
20
|
-
const { text, url } = menuItem
|
|
21
|
-
return <NavLink
|
|
22
|
-
isActive={menuItem.active || false}
|
|
23
|
-
key={stableIdx}
|
|
24
|
-
text={text}
|
|
25
|
-
url={url}
|
|
26
|
-
/>
|
|
27
|
-
})}
|
|
28
|
-
</ul>
|
|
29
|
-
)
|
|
30
|
-
}
|
|
31
|
-
function VerticalDivider() {
|
|
32
|
-
return (
|
|
33
|
-
<li>
|
|
34
|
-
<span className="bg-gray-400 dark:bg-gray-200 w-0.5 h-6 mx-1 block" />
|
|
35
|
-
</li>
|
|
36
|
-
)
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
function NavLink(props: { text: string; url: string; isActive: boolean }) {
|
|
41
|
-
const { text, url, isActive } = props
|
|
42
|
-
return (
|
|
43
|
-
<li>
|
|
44
|
-
<LinkWrap Link={Link} className="px-2" href={url}>
|
|
45
|
-
<NavItemText isActive={isActive} text={text} />
|
|
46
|
-
</LinkWrap>
|
|
47
|
-
</li>
|
|
48
|
-
)
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
export default MenuItemsDefault
|
|
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
|
+
{menuItems.map((menuItem, stableIdx) => {
|
|
17
|
+
if (menuItem === 'divider') {
|
|
18
|
+
return <VerticalDivider key={stableIdx} />
|
|
19
|
+
}
|
|
20
|
+
const { text, url } = menuItem
|
|
21
|
+
return <NavLink
|
|
22
|
+
isActive={menuItem.active || false}
|
|
23
|
+
key={stableIdx}
|
|
24
|
+
text={text}
|
|
25
|
+
url={url}
|
|
26
|
+
/>
|
|
27
|
+
})}
|
|
28
|
+
</ul>
|
|
29
|
+
)
|
|
30
|
+
}
|
|
31
|
+
function VerticalDivider() {
|
|
32
|
+
return (
|
|
33
|
+
<li>
|
|
34
|
+
<span className="bg-gray-400 dark:bg-gray-200 w-0.5 h-6 mx-1 block" />
|
|
35
|
+
</li>
|
|
36
|
+
)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
function NavLink(props: { text: string; url: string; isActive: boolean }) {
|
|
41
|
+
const { text, url, isActive } = props
|
|
42
|
+
return (
|
|
43
|
+
<li>
|
|
44
|
+
<LinkWrap Link={Link} className="px-2" href={url}>
|
|
45
|
+
<NavItemText isActive={isActive} text={text} />
|
|
46
|
+
</LinkWrap>
|
|
47
|
+
</li>
|
|
48
|
+
)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
export default MenuItemsDefault
|
|
@@ -1,51 +1,51 @@
|
|
|
1
|
-
import type { ReactElement } from 'react';
|
|
2
|
-
import React from 'react'
|
|
3
|
-
import LinkWrap from '@dbosoft/react-uicore/link-wrap'
|
|
4
|
-
import CtaLinks from '../CtaLinks'
|
|
5
|
-
import type { ICtaItem, MenuItem } from '../..'
|
|
6
|
-
import s from './style.module.scss'
|
|
7
|
-
import Link from 'next/link';
|
|
8
|
-
import clsx from 'clsx'
|
|
9
|
-
|
|
10
|
-
interface MenuItemsOverflowProps {
|
|
11
|
-
menuItems: MenuItem[],
|
|
12
|
-
ctaLinks: ICtaItem[],
|
|
13
|
-
hideGithubStars: boolean,
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
export default function MenuItemsOverflow({ menuItems, ctaLinks, hideGithubStars }: MenuItemsOverflowProps) {
|
|
18
|
-
return (<><ul className={s.ulElem}>
|
|
19
|
-
{menuItems.map((menuItem, stableIdx) => {
|
|
20
|
-
if (menuItem === 'divider') return null
|
|
21
|
-
|
|
22
|
-
const { text, url } = menuItem
|
|
23
|
-
return (
|
|
24
|
-
<SubmenuItem
|
|
25
|
-
active={menuItem.active || false}
|
|
26
|
-
key={stableIdx}
|
|
27
|
-
text={text}
|
|
28
|
-
url={url}
|
|
29
|
-
/>
|
|
30
|
-
)
|
|
31
|
-
})}
|
|
32
|
-
</ul>
|
|
33
|
-
<CtaLinks
|
|
34
|
-
hideGithubStars={hideGithubStars}
|
|
35
|
-
isInDropdown
|
|
36
|
-
links={ctaLinks}
|
|
37
|
-
/></>)
|
|
38
|
-
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function SubmenuItem({ url, text, active }: { url: string; text: string; active: boolean }) {
|
|
42
|
-
|
|
43
|
-
return (
|
|
44
|
-
<li>
|
|
45
|
-
<LinkWrap Link={Link} className={clsx(s.submenuItem, "text-black dark:text-white" )} href={url} title={text}>
|
|
46
|
-
{text}
|
|
47
|
-
</LinkWrap>
|
|
48
|
-
</li>
|
|
49
|
-
)
|
|
50
|
-
}
|
|
51
|
-
|
|
1
|
+
import type { ReactElement } from 'react';
|
|
2
|
+
import React from 'react'
|
|
3
|
+
import LinkWrap from '@dbosoft/react-uicore/link-wrap'
|
|
4
|
+
import CtaLinks from '../CtaLinks'
|
|
5
|
+
import type { ICtaItem, MenuItem } from '../..'
|
|
6
|
+
import s from './style.module.scss'
|
|
7
|
+
import Link from 'next/link';
|
|
8
|
+
import clsx from 'clsx'
|
|
9
|
+
|
|
10
|
+
interface MenuItemsOverflowProps {
|
|
11
|
+
menuItems: MenuItem[],
|
|
12
|
+
ctaLinks: ICtaItem[],
|
|
13
|
+
hideGithubStars: boolean,
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
export default function MenuItemsOverflow({ menuItems, ctaLinks, hideGithubStars }: MenuItemsOverflowProps) {
|
|
18
|
+
return (<><ul className={s.ulElem}>
|
|
19
|
+
{menuItems.map((menuItem, stableIdx) => {
|
|
20
|
+
if (menuItem === 'divider') return null
|
|
21
|
+
|
|
22
|
+
const { text, url } = menuItem
|
|
23
|
+
return (
|
|
24
|
+
<SubmenuItem
|
|
25
|
+
active={menuItem.active || false}
|
|
26
|
+
key={stableIdx}
|
|
27
|
+
text={text}
|
|
28
|
+
url={url}
|
|
29
|
+
/>
|
|
30
|
+
)
|
|
31
|
+
})}
|
|
32
|
+
</ul>
|
|
33
|
+
<CtaLinks
|
|
34
|
+
hideGithubStars={hideGithubStars}
|
|
35
|
+
isInDropdown
|
|
36
|
+
links={ctaLinks}
|
|
37
|
+
/></>)
|
|
38
|
+
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function SubmenuItem({ url, text, active }: { url: string; text: string; active: boolean }) {
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<li>
|
|
45
|
+
<LinkWrap Link={Link} className={clsx(s.submenuItem, "text-black dark:text-white" )} href={url} title={text}>
|
|
46
|
+
{text}
|
|
47
|
+
</LinkWrap>
|
|
48
|
+
</li>
|
|
49
|
+
)
|
|
50
|
+
}
|
|
51
|
+
|
|
@@ -1,29 +1,29 @@
|
|
|
1
|
-
import clsx from 'clsx'
|
|
2
|
-
import React from 'react'
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
*
|
|
6
|
-
* A span of styled text with
|
|
7
|
-
* an active state that adds a thick
|
|
8
|
-
* bottom border.
|
|
9
|
-
*
|
|
10
|
-
*/
|
|
11
|
-
function NavItemText({
|
|
12
|
-
isActive,
|
|
13
|
-
text,
|
|
14
|
-
}: {
|
|
15
|
-
/** If true, item will be rendered with a thick bottom border below the text. */
|
|
16
|
-
isActive: boolean
|
|
17
|
-
/** Plain text to render within the styled <span /> */
|
|
18
|
-
text: string
|
|
19
|
-
}): React.ReactElement {
|
|
20
|
-
return (
|
|
21
|
-
<span
|
|
22
|
-
className={clsx("text-black dark:text-white text-sm font-semibold", isActive ? "border-b-2 pb-2 border-black" : "")}
|
|
23
|
-
>
|
|
24
|
-
{text}
|
|
25
|
-
</span>
|
|
26
|
-
)
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export default NavItemText
|
|
1
|
+
import clsx from 'clsx'
|
|
2
|
+
import React from 'react'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
*
|
|
6
|
+
* A span of styled text with
|
|
7
|
+
* an active state that adds a thick
|
|
8
|
+
* bottom border.
|
|
9
|
+
*
|
|
10
|
+
*/
|
|
11
|
+
function NavItemText({
|
|
12
|
+
isActive,
|
|
13
|
+
text,
|
|
14
|
+
}: {
|
|
15
|
+
/** If true, item will be rendered with a thick bottom border below the text. */
|
|
16
|
+
isActive: boolean
|
|
17
|
+
/** Plain text to render within the styled <span /> */
|
|
18
|
+
text: string
|
|
19
|
+
}): React.ReactElement {
|
|
20
|
+
return (
|
|
21
|
+
<span
|
|
22
|
+
className={clsx("text-black dark:text-white text-sm font-semibold", isActive ? "border-b-2 pb-2 border-black" : "")}
|
|
23
|
+
>
|
|
24
|
+
{text}
|
|
25
|
+
</span>
|
|
26
|
+
)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export default NavItemText
|
package/src/tabs/Tabs.tsx
CHANGED
|
@@ -1,50 +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;
|
|
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;
|
package/src/tabs/TabsClient.tsx
CHANGED
|
@@ -1,75 +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
|
|
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
|
package/src/tabs/index.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
// Client-safe exports — can be imported from 'use client' components
|
|
2
|
-
export type { TabConfig } from './TabsClient'
|
|
3
|
-
export { default as TabsClient, createNavUrl, TabLink } from './TabsClient'
|
|
1
|
+
// Client-safe exports — can be imported from 'use client' components
|
|
2
|
+
export type { TabConfig } from './TabsClient'
|
|
3
|
+
export { default as TabsClient, createNavUrl, TabLink } from './TabsClient'
|
package/src/tabs/server.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
// Server-only exports — must NOT be imported from 'use client' components
|
|
2
|
-
export { default as Tabs, TabsContent } from './Tabs'
|
|
1
|
+
// Server-only exports — must NOT be imported from 'use client' components
|
|
2
|
+
export { default as Tabs, TabsContent } from './Tabs'
|
|
@@ -83,7 +83,7 @@ export function ThemeSelector({
|
|
|
83
83
|
<Listbox as="div" value={theme} onChange={setTheme} {...props}>
|
|
84
84
|
<ListboxLabel className="sr-only">{labels.theme}</ListboxLabel>
|
|
85
85
|
<ListboxButton
|
|
86
|
-
className="flex h-9 w-12 mt-3 items-center justify-center rounded-
|
|
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
87
|
aria-label={labels.themeAriaLabel}
|
|
88
88
|
>
|
|
89
89
|
<LightIcon
|
|
@@ -99,7 +99,7 @@ export function ThemeSelector({
|
|
|
99
99
|
)}
|
|
100
100
|
/>
|
|
101
101
|
</ListboxButton>
|
|
102
|
-
<ListboxOptions className="absolute left-1/2 top-full mt-3 w-36 -translate-x-1/2 space-y-1 rounded-
|
|
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
103
|
{themes.map((theme) => (
|
|
104
104
|
<ListboxOption
|
|
105
105
|
key={theme.value}
|
|
@@ -118,7 +118,7 @@ export function ThemeSelector({
|
|
|
118
118
|
>
|
|
119
119
|
{({ selected }) => (
|
|
120
120
|
<>
|
|
121
|
-
<div className="rounded-
|
|
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
122
|
<theme.icon
|
|
123
123
|
className={clsx(
|
|
124
124
|
'h-4 w-4',
|