feat: extract theme module with build-time overrides#79
Conversation
There was a problem hiding this comment.
Pull request overview
Extracts a build-time theming system for the console by introducing a standardized theme manifest, build-time same-path overrides, and locale override merging, then wiring theme metadata into core UI entry points.
Changes:
- Added theme manifest normalization/loading and updated UI surfaces (metadata, nav, auth/config pages, sidebar, social links) to use theme-provided brand/link values.
- Implemented build/dev-time theme override application (components/assets/public + manifest copy) and added built-in
defaultandexamplethemes. - Added theme locale override fetching and key-level merge into i18n resources.
Reviewed changes
Copilot reviewed 25 out of 31 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| types/theme.d.ts | Adds ThemeManifest type used by the new theme module. |
| scripts/apply-theme-overrides.mjs | Applies and rolls back theme overrides + copies theme manifest into config at build/dev time. |
| package.json | Runs the theme override script automatically for dev and build. |
| lib/theme/manifest.ts | Loads and normalizes config/theme-manifest.json into a typed manifest API. |
| lib/theme/locales.ts | Loads theme locale overrides from a public URL path. |
| lib/i18n.ts | Merges base locale resources with theme override keys during i18n initialization. |
| config/theme-manifest.json | Provides the default theme manifest used when no overrides are applied. |
| config/navs.ts | Uses the theme manifest to override the Documentation link target. |
| components/user/dropdown.tsx | Uses theme brand name for avatar alt text (and introduces theme access in dropdown). |
| components/tiers/new-form.tsx | Uses theme brand name when selected type is rustfs. |
| components/sidebars/version.tsx | Defaults displayed app name to the theme short name. |
| components/links/x.tsx | Uses theme-provided X link (and hides the link if unset). |
| components/links/github.tsx | Uses theme-provided GitHub link (and hides the link if unset). |
| components/auth/login-form.tsx | Uses theme brand name for logo alt + theme website link for “Get Help”. |
| components/app-sidebar.tsx | Uses theme brand name for sidebar app name and logo alt. |
| app/layout.tsx | Uses theme brand name/description for Next metadata. |
| app/(auth)/config/page.tsx | Uses theme brand name for logo alt + theme docs link for “Need help?”. |
| themes/default/manifest.json | Adds default theme manifest. |
| themes/default/assets/logo.svg | Adds default theme logo asset. |
| themes/default/components/.gitkeep | Placeholder to keep theme components dir in repo. |
| themes/default/locales/.gitkeep | Placeholder to keep theme locales dir in repo. |
| themes/default/public/.gitkeep | Placeholder to keep theme public dir in repo. |
| themes/example/manifest.json | Adds example theme manifest (brand + links). |
| themes/example/assets/logo.svg | Adds example theme logo asset for same-path override. |
| themes/example/components/auth/heroes/hero-static.tsx | Example same-path component override for the auth hero. |
| themes/example/locales/en-US.json | Example locale overrides (en-US). |
| themes/example/locales/zh-CN.json | Example locale overrides (zh-CN). |
| themes/example/public/backgrounds/scillate.svg | Example public asset override. |
| themes/example/public/backgrounds/ttten.svg | Example public asset override. |
| themes/example/README.md | Documents how the example theme validates override capabilities. |
| .gitignore | Ignores runtime theme artifacts and restricts committed themes to default/example. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
scripts/apply-theme-overrides.mjs
Outdated
| const themeId = process.env.NEXT_PUBLIC_THEME_ID?.trim() || process.env.THEME_ID?.trim() || "default" | ||
| const themeRoot = path.join(root, "themes", themeId) | ||
|
|
There was a problem hiding this comment.
Great catch. I added strict themeId validation (^[A-Za-z0-9_-]+$) and resolve/path-boundary checks to ensure it cannot escape the themes/ directory.
| restorePreviousOverrides() | ||
|
|
||
| for (const mapping of mappings) { | ||
| copyDirRecursive(path.join(themeRoot, mapping.from), path.join(root, mapping.to)) | ||
| } | ||
|
|
||
| const themeManifestPath = path.join(themeRoot, "manifest.json") | ||
| const targetManifestPath = path.join(root, "config", "theme-manifest.json") | ||
|
|
||
| if (fs.existsSync(themeManifestPath)) { | ||
| backupBeforeWrite(targetManifestPath) | ||
| ensureParentDir(targetManifestPath) | ||
| fs.copyFileSync(themeManifestPath, targetManifestPath) | ||
| } | ||
|
|
||
| writeState(stateEntries) | ||
|
|
There was a problem hiding this comment.
Addressed. The script now writes an initial empty state before apply and persists state incrementally on each backup (backupBeforeWrite), so rollback metadata survives mid-run failures.
| return { | ||
| schemaVersion: THEME_SCHEMA_VERSION, | ||
| id, | ||
| name, |
There was a problem hiding this comment.
Fixed. schemaVersion is now read from manifest and validated strictly; mismatches throw with a clear error message instead of silently normalizing.
| for (const [code, file] of Object.entries(LOCALE_FILE_MAP)) { | ||
| const baseLocale = await loadLocale(file) | ||
| const themeOverride = await loadThemeLocaleOverride(file) | ||
|
|
||
| resources[code] = { | ||
| translation: await loadLocale(file), | ||
| translation: themeOverride ? { ...baseLocale, ...themeOverride } : baseLocale, | ||
| } |
There was a problem hiding this comment.
Improved. Theme locale loading now uses a generated index.json to request only existing override locale files, avoiding per-locale 404 fetches at startup.
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
b55ef04 to
3a264d7
Compare
Pull Request
Description
Extract a theme module for
consolewith a standardizedmanifest.jsonand build-time override workflow.This PR adds:
components,assets, andpublicdefaultand a full-capabilityexampleType of Change
Testing
Note:
pnpm test:runis not available in this repository (Command "test:run" not found).Checklist
Related Issues
Closes #
Screenshots (if applicable)
N/A (infrastructure/theme system changes).
Additional Notes
Key files:
scripts/apply-theme-overrides.mjsconfig/theme-manifest.jsonlib/theme/manifest.tslib/theme/locales.tsthemes/README.mdthemes/default/*themes/example/*