@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.
Files changed (30) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/eslint.config.mjs +3 -3
  3. package/package.json +11 -6
  4. package/src/head/index.tsx +119 -119
  5. package/src/subnav/helpers/useStuckRef.ts +50 -50
  6. package/src/subnav/index.tsx +134 -135
  7. package/src/subnav/partials/CtaLinks/github-stars-link/formatStarCount/index.test.js +25 -25
  8. package/src/subnav/partials/CtaLinks/github-stars-link/formatStarCount/index.ts +17 -17
  9. package/src/subnav/partials/CtaLinks/github-stars-link/index.tsx +96 -96
  10. package/src/subnav/partials/CtaLinks/github-stars-link/parseGithubUrl/index.test.js +25 -25
  11. package/src/subnav/partials/CtaLinks/github-stars-link/parseGithubUrl/index.ts +25 -25
  12. package/src/subnav/partials/CtaLinks/icons/github.svg +3 -3
  13. package/src/subnav/partials/CtaLinks/index.tsx +46 -46
  14. package/src/subnav/partials/MenuItemsDefault/index.tsx +52 -52
  15. package/src/subnav/partials/MenuItemsDefault/style.module.scss +71 -71
  16. package/src/subnav/partials/MenuItemsOverflow/index.tsx +51 -51
  17. package/src/subnav/partials/MenuItemsOverflow/style.module.scss +51 -51
  18. package/src/subnav/partials/TitleLink/index.tsx +25 -25
  19. package/src/subnav/partials/nav-item-text/index.tsx +29 -29
  20. package/src/subnav/partials/nav-item-text/style.module.scss +19 -19
  21. package/src/subnav/style.module.scss +13 -13
  22. package/src/tabs/Tabs.tsx +50 -50
  23. package/src/tabs/TabsClient.tsx +75 -75
  24. package/src/tabs/index.ts +3 -3
  25. package/src/tabs/server.ts +2 -2
  26. package/src/themeselector/index.tsx +139 -139
  27. package/src/translations.ts +25 -25
  28. package/tsconfig.json +7 -7
  29. package/.turbo/turbo-build.log +0 -4
  30. 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;
@@ -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'
@@ -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-lg shadow-md 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-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">
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-md bg-white p-1 shadow 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
+ }
@@ -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
+ }