diff --git a/.storybook/preview.tsx b/.storybook/preview.tsx index 8cfa1e4a..5a46634b 100644 --- a/.storybook/preview.tsx +++ b/.storybook/preview.tsx @@ -52,14 +52,14 @@ const preview: Preview = { }, ], globalTypes: { - darkMode: { - description: 'Toggle dark mode', - defaultValue: 'dark', + theme: { + description: 'Theme for the components', toolbar: { - icon: 'circlehollow', + icon: 'paintbrush', items: [ - { value: 'light', right: '☀️', title: 'Light Mode' }, - { value: 'dark', right: '🌙', title: 'Dark Mode' }, + { value: 'dark', right: '🌙', title: 'Dark' }, // default + { value: 'light', right: '☀️', title: 'Light' }, + { value: 'system', right: '🖥️', title: 'System' }, ], }, }, diff --git a/README.md b/README.md index d7d70ec8..1941e13f 100644 --- a/README.md +++ b/README.md @@ -65,6 +65,7 @@ See [docs/chromatic-setup.md](docs/chromatic-setup.md) for more details on our C #### Update PR Branches We provide a GitHub Actions workflow to automatically update open PR branches with the latest changes from `main`. This is useful for: + - Keeping long-running PRs up-to-date - Reducing merge conflicts - Repository maintenance diff --git a/components/Header/Header.tsx b/components/Header/Header.tsx index 0da552a1..c1c2be72 100644 --- a/components/Header/Header.tsx +++ b/components/Header/Header.tsx @@ -6,8 +6,10 @@ import React from 'react' import { FaDiscord, FaGithub } from 'react-icons/fa' import logoBluePng from '@/src/assets/images/logo_blue.png' import { useNextTranslation } from '@/src/hooks/i18n' +import { themeConfig } from '@/utils/themeConfig' import { useFromUrlParam } from '../common/HOC/useFromUrl' import LanguageSwitcher from '../common/LanguageSwitcher' +import ThemeSwitcher from '../common/ThemeSwitcher' import ProfileDropdown from './ProfileDropdown' interface HeaderProps { @@ -32,9 +34,8 @@ const Header: React.FC = ({ isLoggedIn, title }) => { return ( = ({ isLoggedIn, title }) => { height={36} className="w-6 h-6 mr-3 sm:w-9 sm:h-9 rounded-lg" /> - + {t('Comfy Registry')} -
+
{isLoggedIn ? ( ) : ( <> - - )} @@ -73,16 +86,17 @@ const Header: React.FC = ({ isLoggedIn, title }) => { ? 'https://docs.comfy.org/zh-CN' : 'https://docs.comfy.org/registry/overview' } - color="blue" size="xs" + className={`${themeConfig.button.documentation} border-0`} > - + {t('Documentation')} + {/* place in the most-right to reduce ... when switching language */}
diff --git a/components/Header/ProfileDropdown.tsx b/components/Header/ProfileDropdown.tsx index 75806e50..7634e684 100644 --- a/components/Header/ProfileDropdown.tsx +++ b/components/Header/ProfileDropdown.tsx @@ -5,6 +5,7 @@ import { HiChevronDown } from 'react-icons/hi' import { useGetUser } from '@/src/api/generated' import { useNextTranslation } from '@/src/hooks/i18n' import { useFirebaseUser } from '@/src/hooks/useFirebaseUser' +import { themeConfig } from '@/utils/themeConfig' import { useLogout } from '../AuthUI/Logout' export default function ProfileDropdown() { @@ -13,6 +14,17 @@ export default function ProfileDropdown() { const [onSignOut, isSignoutLoading, error] = useLogout() const { data: user } = useGetUser() + // Custom theme for dropdown to match our theme configuration + const customDropdownTheme = { + floating: { + base: `z-10 w-fit rounded divide-y divide-gray-100 shadow focus:outline-none ${themeConfig.dropdown.background} ${themeConfig.dropdown.border}`, + content: 'py-1 text-sm', + item: { + base: `flex items-center justify-start py-2 px-4 text-sm cursor-pointer w-full ${themeConfig.dropdown.item}`, + }, + }, + } + // // debug // return <>{JSON.stringify(useFirebaseUser(), null, 2)} const [firebaseUser] = useFirebaseUser() @@ -22,6 +34,7 @@ export default function ProfileDropdown() { = ({ hit }) => { )?.filter((e) => (e.matchedWords as string[])?.length) return (
-
+
@@ -51,7 +51,7 @@ const Hit: React.FC = ({ hit }) => { ( -

+

{children}

), @@ -64,7 +64,7 @@ const Hit: React.FC = ({ hit }) => { {/* nodes */} {hit.comfy_nodes?.length && ( -
+
@@ -95,7 +95,7 @@ const Hit: React.FC = ({ hit }) => {
{/* meta info */}

diff --git a/components/common/GenericHeader.tsx b/components/common/GenericHeader.tsx index 8739934b..9dea974c 100644 --- a/components/common/GenericHeader.tsx +++ b/components/common/GenericHeader.tsx @@ -21,10 +21,10 @@ const GenericHeader: React.FC = ({ }) => { return ( <> -

+

{title}

-

+

{subTitle}

diff --git a/components/common/ThemeSwitcher.tsx b/components/common/ThemeSwitcher.tsx new file mode 100644 index 00000000..6b017922 --- /dev/null +++ b/components/common/ThemeSwitcher.tsx @@ -0,0 +1,92 @@ +import { Dropdown, DropdownItem } from 'flowbite-react' +import React from 'react' +import { HiDesktopComputer, HiMoon, HiSun } from 'react-icons/hi' +import { useNextTranslation } from '@/src/hooks/i18n' +import { Theme, useTheme } from '@/src/hooks/useTheme' + +const ThemeIcon = ({ + theme, + actualTheme, +}: { + theme: Theme + actualTheme: 'light' | 'dark' +}) => { + const icons = { + auto: , + light: , + dark: , + } satisfies Record + + if (theme in icons) { + return icons[theme] + } + throw new Error(`Unknown theme: ${theme}`) +} + +export default function ThemeSwitcher() { + const { theme, actualTheme, setTheme } = useTheme() + const { t } = useNextTranslation() + + const themeOptions: { + value: Theme + label: string + icon: React.ReactNode + }[] = [ + { + value: 'auto', + label: t('Auto'), + icon: , + }, + { + value: 'light', + label: t('Light'), + icon: , + }, + { + value: 'dark', + label: t('Dark'), + icon: , + }, + ] + + const currentOption = themeOptions.find((option) => option.value === theme) + + // Handle double-click to toggle between light and dark themes + const handleDoubleClick = () => { + if (actualTheme === 'light') { + setTheme('dark') + } else { + setTheme('light') + } + } + + return ( + ( + + )} + color="gray" + size="xs" + dismissOnClick + > + {themeOptions.map((option) => ( + setTheme(option.value)} + className={`flex items-center gap-2 ${theme === option.value ? 'font-bold' : ''}`} + > + {option.icon} + {option.label} + + ))} + + ) +} diff --git a/components/common/UnifiedBreadcrumb.tsx b/components/common/UnifiedBreadcrumb.tsx new file mode 100644 index 00000000..34d05287 --- /dev/null +++ b/components/common/UnifiedBreadcrumb.tsx @@ -0,0 +1,92 @@ +import { Breadcrumb } from 'flowbite-react' +import { useRouter } from 'next/router' +import React, { FC, SVGProps } from 'react' +import { HiHome } from 'react-icons/hi' +import { themeConfig } from '@/utils/themeConfig' + +interface BreadcrumbItem { + label: string + href?: string + onClick?: (e: React.MouseEvent) => void + icon?: FC> + isActive?: boolean +} + +interface UnifiedBreadcrumbProps { + items: BreadcrumbItem[] + className?: string +} + +const UnifiedBreadcrumb: React.FC = ({ + items, + className = '', +}) => { + const router = useRouter() + + return ( + + {items.map((item, index) => ( + + {item.label} + + ))} + + ) +} + +export { UnifiedBreadcrumb } +export default UnifiedBreadcrumb + +export const createHomeBreadcrumb = (t: (key: string) => string) => ({ + label: t('Home'), + href: '/', + icon: HiHome, + onClick: (e: React.MouseEvent) => { + e.preventDefault() + window.location.href = '/' + }, +}) + +export const createNodesBreadcrumb = (t: (key: string) => string) => ({ + label: t('Your Nodes'), +}) + +export const createAllNodesBreadcrumb = (t: (key: string) => string) => ({ + label: t('All Nodes'), +}) + +export const createNodeDetailBreadcrumb = (nodeId: string) => ({ + label: nodeId, + isActive: true, +}) + +export const createPublisherBreadcrumb = (publisherName: string) => ({ + label: publisherName, + isActive: true, +}) + +export const createAdminDashboardBreadcrumb = ( + t: (key: string) => string, + isCurrentPage = false +) => ({ + label: t('Admin Dashboard'), + href: isCurrentPage ? undefined : '/admin', + onClick: isCurrentPage + ? undefined + : (e: React.MouseEvent) => { + e.preventDefault() + window.location.href = '/admin' + }, +}) + +export const createUnclaimedNodesBreadcrumb = (t: (key: string) => string) => ({ + label: t('Unclaimed Nodes'), +}) diff --git a/components/nodes/NodeDetails.tsx b/components/nodes/NodeDetails.tsx index 66ad713f..31e31b32 100644 --- a/components/nodes/NodeDetails.tsx +++ b/components/nodes/NodeDetails.tsx @@ -23,6 +23,7 @@ import { } from '@/src/api/generated' import nodesLogo from '@/src/assets/images/nodesLogo.svg' import { useNextTranslation } from '@/src/hooks/i18n' +import { themeConfig } from '@/utils/themeConfig' import CopyableCodeBlock from '../CodeBlock/CodeBlock' import { NodeDeleteModal } from './NodeDeleteModal' import { NodeEditModal } from './NodeEditModal' @@ -209,19 +210,23 @@ const NodeDetails = () => { // TODO: show error message and allow navigate back to the list return (
-
+

{t('Error loading node details')}

{/* reason */} -

+

{t('Reason')}:{' '} {t('An unexpected error occurred. Please try again later.')}

{process.env.NODE_ENV === 'development' && ( -

+

{t('Debug info')}: {error.message}

)} @@ -240,7 +245,9 @@ const NodeDetails = () => { if (!node) { return (
-
+

@@ -256,7 +263,9 @@ const NodeDetails = () => { return ( <> {/* TODO(sno): unwrap this div out of fragment in another PR */} -
+
{
-

{node.name}

+

+ {node.name} +

)} */} {node.downloads != 0 && ( -

+

<> {isUnclaimed || !nodeVersions?.length ? ( -

+

{isUnclaimed ? t( "This node can only be installed via git, because it's unclaimed by any publisher" @@ -441,28 +458,42 @@ const NodeDetails = () => {

-

{t('Description')}

-

+

+ {t('Description')} +

+

{node.description}