diff --git a/.gitignore b/.gitignore index ecc8282..88406be 100644 --- a/.gitignore +++ b/.gitignore @@ -30,14 +30,10 @@ *.pem .theme-runtime -# themes: keep only default and example in repo +# themes: keep only default in repo /themes/* !/themes/default/ !/themes/default/** -!/themes/example/ -!/themes/example/** -!/themes/.active/ -!/themes/.active/** # generated theme locale artifacts /public/themes/ diff --git a/app/(auth)/config/page.tsx b/app/(auth)/config/page.tsx index 402483a..5501e87 100644 --- a/app/(auth)/config/page.tsx +++ b/app/(auth)/config/page.tsx @@ -10,13 +10,13 @@ import { Field, FieldContent, FieldDescription, FieldLabel } from "@/components/ import { Input } from "@/components/ui/input" import { ThemeSwitcher } from "@/components/theme-switcher" import { LanguageSwitcher } from "@/components/language-switcher" +import { ThemeLogo } from "@/components/theme/logo" import { AuthHeroStatic } from "@/components/auth/heroes/hero-static" import { useMessage } from "@/lib/feedback/message" import { buildRoute, getLoginRoute } from "@/lib/routes" import { configManager } from "@/lib/config" import { checkServerHealth } from "@/lib/config-helpers" import { getThemeManifest } from "@/lib/theme/manifest" -import logoImage from "@/assets/logo.svg" export default function ConfigPage() { return ( @@ -114,7 +114,7 @@ function ConfigPageContent() {
diff --git a/assets/logo-dark.svg b/assets/logo-dark.svg new file mode 100644 index 0000000..5d54270 --- /dev/null +++ b/assets/logo-dark.svg @@ -0,0 +1,8 @@ + diff --git a/components/app-sidebar.tsx b/components/app-sidebar.tsx index bac453c..52d037b 100644 --- a/components/app-sidebar.tsx +++ b/components/app-sidebar.tsx @@ -1,7 +1,6 @@ "use client" import Link from "next/link" -import Image from "next/image" import { usePathname } from "next/navigation" import { useTranslation } from "react-i18next" import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible" @@ -26,12 +25,12 @@ import { import { RiArrowRightSLine, RiExternalLinkLine } from "@remixicon/react" import { getIconComponent } from "@/lib/icon-map" import navs from "@/config/navs" -import logoImage from "@/assets/logo.svg" import type { NavItem } from "@/types/app-config" import { usePermissions } from "@/hooks/use-permissions" import { SidebarVersion } from "@/components/sidebars/version" import { useDirection } from "@/components/ui/direction" import { getThemeManifest } from "@/lib/theme/manifest" +import { ThemeLogo } from "@/components/theme/logo" const theme = getThemeManifest() const APP_NAME = theme.brand.name @@ -135,7 +134,7 @@ export function AppSidebar() {
{t(item.labelKey)}
{t(item.descKey)}
@@ -177,15 +173,7 @@ export function TiersNewForm({ open, onOpenChange, onSuccess }: TiersNewFormProp className="w-full cursor-pointer border text-left transition hover:border-primary" >{t("Selected Type")}
{type === "rustfs" ? theme.brand.name : type}
diff --git a/lib/theme/assets.ts b/lib/theme/assets.ts new file mode 100644 index 0000000..0732aac --- /dev/null +++ b/lib/theme/assets.ts @@ -0,0 +1,31 @@ +import { buildRoute } from "@/lib/routes" + +function isAbsoluteUrl(path: string): boolean { + return /^[a-z][a-z0-9+.-]*:/i.test(path) +} + +export function resolveThemeAssetPath(path: string): string { + if (isAbsoluteUrl(path)) return path + return buildRoute(path) +} + +export function withDarkVariant(path: string): string { + if (isAbsoluteUrl(path)) return path + + const [pathname, suffix = ""] = path.split(/(?=[?#])/) + const lastSlashIndex = pathname.lastIndexOf("/") + const fileName = lastSlashIndex >= 0 ? pathname.slice(lastSlashIndex + 1) : pathname + const dirName = lastSlashIndex >= 0 ? pathname.slice(0, lastSlashIndex + 1) : "" + const dotIndex = fileName.lastIndexOf(".") + const baseName = dotIndex > 0 ? fileName.slice(0, dotIndex) : fileName + const ext = dotIndex > 0 ? fileName.slice(dotIndex) : "" + + if (baseName.endsWith("-dark")) return path + + return `${dirName}${baseName}-dark${ext}${suffix}` +} + +export function themedPath(path: string, options?: { dark?: boolean }): string { + const themed = options?.dark ? withDarkVariant(path) : path + return resolveThemeAssetPath(themed) +} diff --git a/scripts/apply-theme-overrides.mjs b/scripts/apply-theme-overrides.mjs index 55b7e3c..1126933 100644 --- a/scripts/apply-theme-overrides.mjs +++ b/scripts/apply-theme-overrides.mjs @@ -158,6 +158,14 @@ for (const mapping of mappings) { copyDirRecursive(path.join(themeRoot, mapping.from), path.join(root, mapping.to)) } +const themeLogoPath = path.join(root, "assets", "logo.svg") +const themeDarkLogoPath = path.join(root, "assets", "logo-dark.svg") +if (fs.existsSync(themeLogoPath) && !fs.existsSync(themeDarkLogoPath)) { + backupBeforeWrite(themeDarkLogoPath) + ensureParentDir(themeDarkLogoPath) + fs.copyFileSync(themeLogoPath, themeDarkLogoPath) +} + const themeLocaleRoot = path.join(themeRoot, "locales") const localeFiles = listLocaleFiles(themeLocaleRoot) const localeIndex = localeFiles.map((file) => file.replace(/\.json$/, "")) diff --git a/themes/.active/manifest.json b/themes/.active/manifest.json deleted file mode 100644 index 9c62c20..0000000 --- a/themes/.active/manifest.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "schemaVersion": 1, - "id": "default", - "name": "Default", - "brand": { - "name": "RustFS", - "shortName": "RustFS", - "description": "RustFS is a distributed file system written in Rust." - }, - "assets": { - "logo": "/themes/default/assets/logo.svg", - "logoCompact": "/themes/default/assets/logo.svg", - "userAvatar": "/img/userAvatar.png" - }, - "links": { - "website": "https://www.rustfs.com", - "documentation": "https://docs.rustfs.com/", - "github": "https://github.com/rustfs/console", - "x": "https://x.com/rustfsofficial" - } -} diff --git a/themes/example/README.md b/themes/example/README.md deleted file mode 100644 index 0ff1298..0000000 --- a/themes/example/README.md +++ /dev/null @@ -1,33 +0,0 @@ -# Example Theme Verification - -This example theme validates all customization capabilities: - -1. `manifest`: - - Brand name and links are replaced from `themes/example/manifest.json`. -2. `components` (same-path override): - - `themes/example/components/auth/heroes/hero-static.tsx` - - Overrides `components/auth/heroes/hero-static.tsx`. -3. `assets` (same-path override): - - `themes/example/assets/logo.svg` - - Overrides `assets/logo.svg`. -4. `locales` (key-level merge override): - - `themes/example/locales/en-US.json` - - `themes/example/locales/zh-CN.json` - - Only specified keys are overridden. -5. `public` (same-path override): - - `themes/example/public/backgrounds/scillate.svg` - - `themes/example/public/backgrounds/ttten.svg` - -## Quick Check - -```bash -NEXT_PUBLIC_THEME_ID=example pnpm dev -``` - -Then verify on login/config pages: - -- Logo shows `ACME Console`. -- Hero content is the custom Acme component. -- Background is teal style (from `public/backgrounds/*` override). -- Brand links use `example.com` / `docs.example.com`. -- Texts such as `Login`, `View Documentation` are overridden, while unspecified i18n keys still use default translations. diff --git a/themes/example/assets/logo.svg b/themes/example/assets/logo.svg deleted file mode 100644 index 1d41a84..0000000 --- a/themes/example/assets/logo.svg +++ /dev/null @@ -1,8 +0,0 @@ - diff --git a/themes/example/components/auth/heroes/hero-static.tsx b/themes/example/components/auth/heroes/hero-static.tsx deleted file mode 100644 index 65932fd..0000000 --- a/themes/example/components/auth/heroes/hero-static.tsx +++ /dev/null @@ -1,42 +0,0 @@ -"use client" - -import Image from "next/image" -import Link from "next/link" -import { useTranslation } from "react-i18next" -import { RiArrowRightLongFill } from "@remixicon/react" -import { FlipWords } from "@/components/ui/flip-words" -import logoImage from "@/assets/logo.svg" -import { buildRoute } from "@/lib/routes" -import { getThemeManifest } from "@/lib/theme/manifest" - -export function AuthHeroStatic() { - const { t } = useTranslation() - const manifest = getThemeManifest() - - return ( -