@dbosoft/nextjs-uicore 1.5.1 → 1.6.1
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 +18 -0
- package/eslint.config.mjs +3 -3
- package/package.json +11 -6
- package/src/head/index.tsx +119 -119
- package/src/subnav/helpers/useStuckRef.ts +50 -50
- package/src/subnav/index.tsx +134 -135
- package/src/subnav/partials/CtaLinks/github-stars-link/formatStarCount/index.test.js +25 -25
- 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.test.js +25 -25
- package/src/subnav/partials/CtaLinks/github-stars-link/parseGithubUrl/index.ts +25 -25
- package/src/subnav/partials/CtaLinks/icons/github.svg +3 -3
- package/src/subnav/partials/CtaLinks/index.tsx +46 -46
- package/src/subnav/partials/MenuItemsDefault/index.tsx +52 -52
- package/src/subnav/partials/MenuItemsDefault/style.module.scss +71 -71
- package/src/subnav/partials/MenuItemsOverflow/index.tsx +51 -51
- 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/index.tsx +29 -29
- package/src/subnav/partials/nav-item-text/style.module.scss +19 -19
- package/src/subnav/style.module.scss +13 -13
- 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 +139 -139
- package/src/translations.ts +25 -25
- package/tsconfig.json +7 -7
- package/.turbo/turbo-build.log +0 -4
- package/.turbo/turbo-check-types.log +0 -4
|
@@ -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
|
+
}
|
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'
|
|
@@ -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-
|
|
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-
|
|
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-
|
|
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/src/translations.ts
CHANGED
|
@@ -1,25 +1,25 @@
|
|
|
1
|
-
import type { ThemeSelectorLabels } from './themeselector'
|
|
2
|
-
import type { SubNavLabels } from './subnav'
|
|
3
|
-
import type { GithubStarsLinkLabels } from './subnav/partials/CtaLinks/github-stars-link'
|
|
4
|
-
|
|
5
|
-
export const themeSelectorLabels: { de: ThemeSelectorLabels } = {
|
|
6
|
-
de: {
|
|
7
|
-
light: 'Hell',
|
|
8
|
-
dark: 'Dunkel',
|
|
9
|
-
system: 'System',
|
|
10
|
-
theme: 'Design',
|
|
11
|
-
themeAriaLabel: 'Design',
|
|
12
|
-
},
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export const subNavLabels: { de: SubNavLabels } = {
|
|
16
|
-
de: {
|
|
17
|
-
openMainMenu: 'Hauptmenü öffnen',
|
|
18
|
-
},
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export const githubStarsLinkLabels: { de: GithubStarsLinkLabels } = {
|
|
22
|
-
de: {
|
|
23
|
-
github: 'Github',
|
|
24
|
-
},
|
|
25
|
-
}
|
|
1
|
+
import type { ThemeSelectorLabels } from './themeselector'
|
|
2
|
+
import type { SubNavLabels } from './subnav'
|
|
3
|
+
import type { GithubStarsLinkLabels } from './subnav/partials/CtaLinks/github-stars-link'
|
|
4
|
+
|
|
5
|
+
export const themeSelectorLabels: { de: ThemeSelectorLabels } = {
|
|
6
|
+
de: {
|
|
7
|
+
light: 'Hell',
|
|
8
|
+
dark: 'Dunkel',
|
|
9
|
+
system: 'System',
|
|
10
|
+
theme: 'Design',
|
|
11
|
+
themeAriaLabel: 'Design',
|
|
12
|
+
},
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const subNavLabels: { de: SubNavLabels } = {
|
|
16
|
+
de: {
|
|
17
|
+
openMainMenu: 'Hauptmenü öffnen',
|
|
18
|
+
},
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const githubStarsLinkLabels: { de: GithubStarsLinkLabels } = {
|
|
22
|
+
de: {
|
|
23
|
+
github: 'Github',
|
|
24
|
+
},
|
|
25
|
+
}
|