diff --git a/README.md b/README.md index 9fdc27cd..5081990d 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/bun.lock b/bun.lock index b409d0d4..f2cdd6de 100644 --- a/bun.lock +++ b/bun.lock @@ -1,5 +1,6 @@ { "lockfileVersion": 1, + "configVersion": 0, "workspaces": { "": { "name": "comfy-org-registry-web", diff --git a/components/Header/Header.tsx b/components/Header/Header.tsx index e8f85948..6c26b370 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 ? (
@@ -60,22 +65,20 @@ const Header: React.FC = ({ isLoggedIn, title }) => { <> )} @@ -85,11 +88,10 @@ const Header: React.FC = ({ isLoggedIn, title }) => { ? 'https://docs.comfy.org/zh-CN' : 'https://docs.comfy.org/registry/overview' } - color="blue" size="xs" - className="h-10" + className={`${themeConfig.button.documentation} border-0 h-10`} > - + {t('Documentation')} @@ -104,6 +106,7 @@ const Header: React.FC = ({ isLoggedIn, title }) => { />
+ {/* place in the most-right to reduce ... when switching language */}
diff --git a/components/Header/ProfileDropdown.tsx b/components/Header/ProfileDropdown.tsx index 2e8b92b4..eb4cca28 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}