From b54abcdaa6d1be2ee77bf4fd0f78438a8f2ec79f Mon Sep 17 00:00:00 2001 From: tianhei Date: Tue, 28 Jul 2026 17:20:28 +0800 Subject: [PATCH 1/2] feat(desktop): Raft/home_portal theme + zh/en i18n switch Apply BC home_portal raft tokens (cream paper, charcoal sidebar, amber primary, hard radius, Space Grotesk) across desktop shell styles and shared controls. Add a lightweight Desktop i18n layer with default Chinese, top-chrome and settings language toggles, and main-path coverage for sidebar, settings nav, search, and appearance. Also aligns admin-web/web font/token previews non-blockingly. --- admin-web/index.html | 4 +- admin-web/src/App.tsx | 37 ++- admin-web/src/i18n.ts | 41 +++ admin-web/src/styles.css | 49 ++-- desktop/index.html | 5 + desktop/src/app/AppTopChrome.tsx | 27 +- .../src/features/search/ui/TopbarSearch.tsx | 52 ++-- .../features/settings/ui/SettingsPanels.tsx | 42 ++- .../src/features/settings/ui/SettingsView.tsx | 72 +++-- .../src/features/sidebar/ui/AppSidebar.tsx | 18 +- .../sidebar/ui/AppSidebarPinnedHeader.tsx | 22 +- desktop/src/main.tsx | 25 +- desktop/src/shared/i18n/I18nProvider.tsx | 98 +++++++ desktop/src/shared/i18n/LanguageToggle.tsx | 83 ++++++ desktop/src/shared/i18n/i18n.test.mjs | 42 +++ desktop/src/shared/i18n/index.ts | 11 + desktop/src/shared/i18n/messages.ts | 207 +++++++++++++++ desktop/src/shared/styles/globals.css | 1 + desktop/src/shared/styles/globals/theme.css | 153 +++++------ desktop/src/shared/ui/button.tsx | 9 +- desktop/src/shared/ui/input.tsx | 2 +- desktop/tailwind.config.js | 3 +- preview-ui/index.html | 246 ++++++++++++++++++ web/index.html | 4 +- web/src/shared/styles/globals.css | 141 +++++----- 25 files changed, 1134 insertions(+), 260 deletions(-) create mode 100644 admin-web/src/i18n.ts create mode 100644 desktop/src/shared/i18n/I18nProvider.tsx create mode 100644 desktop/src/shared/i18n/LanguageToggle.tsx create mode 100644 desktop/src/shared/i18n/i18n.test.mjs create mode 100644 desktop/src/shared/i18n/index.ts create mode 100644 desktop/src/shared/i18n/messages.ts create mode 100644 preview-ui/index.html diff --git a/admin-web/index.html b/admin-web/index.html index 961040e83f..324d47a3d7 100644 --- a/admin-web/index.html +++ b/admin-web/index.html @@ -6,7 +6,9 @@ Buzz admin - + + +
diff --git a/admin-web/src/App.tsx b/admin-web/src/App.tsx index f39ecc33c5..baced56f6c 100644 --- a/admin-web/src/App.tsx +++ b/admin-web/src/App.tsx @@ -7,6 +7,7 @@ import { useState, } from "react"; import { ApiFailure, request } from "./api"; +import { getLang, setLang as persistLang, t, type Lang } from "./i18n"; import type { FeedbackDetail, FeedbackSummary, @@ -68,16 +69,16 @@ function StateView({ children: (data: T) => ReactNode; }) { if (resource.loading && !resource.data) - return
Loading…
; + return
{t("loading")}
; if (resource.error && !resource.data) { const forbidden = resource.error instanceof ApiFailure && resource.error.status === 403; return (
-

{forbidden ? "Access denied" : "Could not load data"}

+

{forbidden ? t("access_denied") : t("load_failed")}

{resource.error.message}

); @@ -799,6 +800,7 @@ function ArrowIcon() { export function App() { const { path } = usePath(); + const [lang, setLangState] = useState(() => getLang()); const report = path.match(/^\/reports\/([^/]+)$/); const feedback = path.match(/^\/feedback\/([^/]+)$/); const content = report ? ( @@ -810,24 +812,45 @@ export function App() { ) : ( ); + const switchLang = (next: Lang) => { + persistLang(next); + setLangState(next); + document.documentElement.lang = next === "zh" ? "zh-CN" : "en"; + }; return ( -
+
- Buzz Admin + {t("brand", lang)}
{content}
diff --git a/admin-web/src/i18n.ts b/admin-web/src/i18n.ts new file mode 100644 index 0000000000..72d928c682 --- /dev/null +++ b/admin-web/src/i18n.ts @@ -0,0 +1,41 @@ +export type Lang = "en" | "zh"; + +const dict = { + en: { + brand: "Buzz Admin", + nav_reports: "Reports", + nav_feedback: "Feedback", + loading: "Loading…", + access_denied: "Access denied", + load_failed: "Could not load data", + retry: "Retry", + lang: "Language", + }, + zh: { + brand: "Buzz 管理台", + nav_reports: "举报", + nav_feedback: "反馈", + loading: "加载中…", + access_denied: "无权限", + load_failed: "无法加载数据", + retry: "重试", + lang: "语言", + }, +} as const; + +export type MsgKey = keyof (typeof dict)["en"]; + +const STORAGE_KEY = "buzz-admin-lang"; + +export function getLang(): Lang { + const v = localStorage.getItem(STORAGE_KEY); + return v === "zh" || v === "en" ? v : "zh"; +} + +export function setLang(lang: Lang) { + localStorage.setItem(STORAGE_KEY, lang); +} + +export function t(key: MsgKey, lang: Lang = getLang()): string { + return dict[lang][key] ?? dict.en[key] ?? key; +} diff --git a/admin-web/src/styles.css b/admin-web/src/styles.css index 93ebceeb14..a5b59c6389 100644 --- a/admin-web/src/styles.css +++ b/admin-web/src/styles.css @@ -1,9 +1,11 @@ +@import url("https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@500;600;700;800&display=swap"); + :root { - font-family: - "Helvetica Neue", Helvetica, Arial, ui-sans-serif, system-ui, sans-serif; - color: #231e1e; - background: #d7e7f6; + font-family: "Space Grotesk", "Segoe UI", "PingFang SC", "Helvetica Neue", sans-serif; + color: #1c1917; + background: #fff8f0; font-synthesis: none; + letter-spacing: -0.01em; } * { @@ -29,17 +31,18 @@ select { } button { - color: white; - background: #231e1e; - border: 0; - border-radius: 999px; + color: #fafaf9; + background: #1c1917; + border: 3px solid #1c1917; + border-radius: 0; padding: 0.75rem 1.25rem; cursor: pointer; + font-weight: 700; } .app { min-height: 100vh; - background: linear-gradient(180deg, #d7d72e 0%, #dfe379 24%, #d7e7f6 78%); + background: #fff8f0; } .app-header { @@ -72,8 +75,8 @@ button { display: grid; place-items: center; border-radius: 0.7rem; - color: #d7d72e; - background: #231e1e; + color: #1c1917; + background: #fbbf24; } .brand-mark svg { @@ -86,10 +89,9 @@ nav { align-items: center; gap: 0.35rem; padding: 0.3rem; - border: 1px solid rgb(35 30 30 / 10%); - border-radius: 999px; - background: rgb(255 255 255 / 42%); - backdrop-filter: blur(12px); + border: 3px solid #1c1917; + border-radius: 0; + background: #ffffff; } .nav-link { @@ -732,3 +734,20 @@ dd { margin-top: 0.9rem; } } + +.lang-switch { + display: inline-flex; + gap: 0.25rem; + margin-left: 0.5rem; +} +.lang-switch button { + padding: 0.45rem 0.75rem; + background: #ffffff; + color: #1c1917; + border: 3px solid #1c1917; + border-radius: 0; + font-weight: 700; +} +.lang-switch button.on { + background: #fbbf24; +} diff --git a/desktop/index.html b/desktop/index.html index 4b58bd4326..1aa179b596 100644 --- a/desktop/index.html +++ b/desktop/index.html @@ -54,6 +54,11 @@ } })(); + + diff --git a/desktop/src/app/AppTopChrome.tsx b/desktop/src/app/AppTopChrome.tsx index 845c403b8c..17389d6943 100644 --- a/desktop/src/app/AppTopChrome.tsx +++ b/desktop/src/app/AppTopChrome.tsx @@ -6,6 +6,7 @@ import { PanelLeftOpen, } from "lucide-react"; +import { LanguageToggle, useI18n } from "@/shared/i18n"; import { isMacPlatform } from "@/shared/lib/platform"; import { useIsFullscreen } from "@/shared/lib/useIsFullscreen"; import { Button } from "@/shared/ui/button"; @@ -36,10 +37,11 @@ function preventTopChromeWheel(event: WheelEvent) { function TopChromeSidebarTrigger() { const sidebar = useOptionalSidebar(); + const { t } = useI18n(); return ( ); } @@ -63,6 +65,7 @@ export function AppTopChrome({ onGoForward, hasCommunityRail = false, }: AppTopChromeProps) { + const { t } = useI18n(); const topChromeRef = React.useRef(null); const isFullscreen = useIsFullscreen(); // On macOS the traffic-light buttons overlay the chrome (see @@ -106,10 +109,15 @@ export function AppTopChrome({ data-tauri-drag-region data-testid="app-top-chrome" > -
+
+
+ +
); } diff --git a/desktop/src/features/search/ui/TopbarSearch.tsx b/desktop/src/features/search/ui/TopbarSearch.tsx index 67c4abb6e9..34a7aaf192 100644 --- a/desktop/src/features/search/ui/TopbarSearch.tsx +++ b/desktop/src/features/search/ui/TopbarSearch.tsx @@ -14,6 +14,7 @@ import { } from "@/features/search/ui/SearchResultItem"; import { SearchPromptPlaceholder } from "@/features/search/ui/SearchPromptPlaceholder"; import type { Channel, SearchHit, UserSearchResult } from "@/shared/api/types"; +import { type MsgKey, useI18n } from "@/shared/i18n"; import { cn } from "@/shared/lib/cn"; import { normalizePubkey, truncatePubkey } from "@/shared/lib/pubkey"; import { Dialog, DialogContent, DialogTitle } from "@/shared/ui/dialog"; @@ -195,7 +196,7 @@ function getSearchHitContextLabel( if (channel?.channelType === "dm") { return { channelLabel: null, - text: "Direct message", + text: "Direct message", // translated at call site when needed }; } @@ -211,6 +212,8 @@ function getSearchHitContextLabel( }; } +// Keep English context chips for message hits in v1 — full coverage is phase 2. + function getResultSectionKey(result: SearchResult): SearchResultSectionKey { if (result.kind === "channel") { return result.channel.channelType === "dm" ? "direct-messages" : "channels"; @@ -227,20 +230,23 @@ function getResultSectionKey(result: SearchResult): SearchResultSectionKey { return "messages"; } -function getSectionTitle(sectionKey: SearchResultSectionKey) { +function getSectionTitle( + sectionKey: SearchResultSectionKey, + t: (key: MsgKey) => string, +) { switch (sectionKey) { case "channels": - return "Channels"; + return t("search.channels"); case "direct-messages": - return "Direct messages"; + return t("search.directMessages"); case "people": - return "People"; + return t("search.users"); case "agents": - return "Agents"; + return t("search.agents"); case "messages": - return "Most relevant"; + return t("search.mostRelevant"); case "actions": - return "Actions"; + return t("search.actions"); } } @@ -268,7 +274,10 @@ function SearchHitContextLine({ label }: { label: SearchHitContextLabel }) { ); } -function groupSearchResults(results: SearchResult[]): SearchResultSection[] { +function groupSearchResults( + results: SearchResult[], + t: (key: MsgKey) => string, +): SearchResultSection[] { const resultsBySection = new Map(); for (const result of results) { @@ -289,7 +298,7 @@ function groupSearchResults(results: SearchResult[]): SearchResultSection[] { { key: sectionKey, results: sectionResults, - title: getSectionTitle(sectionKey), + title: getSectionTitle(sectionKey, t), }, ]; }); @@ -400,6 +409,7 @@ export function TopbarSearch({ suggestionChannels, variant = "bar", }: TopbarSearchProps) { + const { t } = useI18n(); const [isOpen, setIsOpen] = React.useState(false); const [selectedMenuIndex, setSelectedMenuIndex] = React.useState(0); const triggerRef = React.useRef(null); @@ -434,7 +444,7 @@ export function TopbarSearch({ kind: "action", action: { id: "browse-channels", - title: "Browse channels", + title: t("search.browseChannels"), }, }); } @@ -444,7 +454,7 @@ export function TopbarSearch({ kind: "action", action: { id: "create-channel", - title: "Create a new channel", + title: t("search.createChannel"), }, }); } @@ -454,13 +464,13 @@ export function TopbarSearch({ kind: "action", action: { id: "create-agent", - title: "Create a new agent", + title: t("search.createAgent"), }, }); } return actions; - }, [onBrowseChannels, onCreateAgent, onCreateChannel]); + }, [onBrowseChannels, onCreateAgent, onCreateChannel, t]); const suggestionResults = React.useMemo( () => [...suggestedResults, ...suggestionActionResults], [suggestedResults, suggestionActionResults], @@ -478,8 +488,8 @@ export function TopbarSearch({ [currentPubkeyNormalized, results], ); const searchResultSections = React.useMemo( - () => groupSearchResults(searchableResults), - [searchableResults], + () => groupSearchResults(searchableResults, t), + [searchableResults, t], ); const groupedSearchResults = React.useMemo( () => searchResultSections.flatMap((section) => section.results), @@ -827,7 +837,7 @@ export function TopbarSearch({
) : null} - {visibleNavGroups.map((group) => ( - - {group.label} - - - {group.sections.map((entry) => ( - - ))} - - - - ))} + {visibleNavGroups.map((group) => { + const groupLabel = t(group.labelKey); + return ( + + {groupLabel} + + + {group.sections.map((entry) => ( + + ))} + + + + ); + })} diff --git a/desktop/src/features/sidebar/ui/AppSidebar.tsx b/desktop/src/features/sidebar/ui/AppSidebar.tsx index 55f467f215..0b9574e1b2 100644 --- a/desktop/src/features/sidebar/ui/AppSidebar.tsx +++ b/desktop/src/features/sidebar/ui/AppSidebar.tsx @@ -1,6 +1,7 @@ // biome-ignore format: keep compact to stay within file size limit import * as React from "react"; import { FeatureGate } from "@/shared/features"; +import { useI18n } from "@/shared/i18n"; import { SidebarDndContext } from "@/features/sidebar/ui/SidebarDnd"; import type { Community } from "@/features/communities/types"; @@ -237,6 +238,7 @@ export function AppSidebar({ onStarChannel, onUnstarChannel, }: AppSidebarProps) { + const { t } = useI18n(); const activeWorkingByChannelId = useActiveWorkingChannelsById(); const { status: updateStatus } = useUpdaterContext(); const canShowSidebarUpdateCard = shouldShowSidebarUpdateCard(updateStatus); @@ -647,7 +649,7 @@ export function AppSidebar({ onSelectChannel={onSelectChannel} onToggleCollapsed={() => toggleCollapsedGroup("starred")} selectedChannelId={selectedChannelId} - title="Starred" + title={t("nav.starred")} unreadChannelCounts={unreadChannelCounts} unreadChannelIds={unreadChannelIds} mutedChannelIds={mutedChannelIds} @@ -743,7 +745,7 @@ export function AppSidebar({ } actionsTestId="section-actions-channels" listTestId="stream-list" - quickCreateLabel="Browse channels" + quickCreateLabel={t("nav.browseChannels")} onQuickCreateClick={() => onBrowseChannels?.()} showQuickCreate onMarkAllRead={onMarkAllChannelsRead} @@ -752,7 +754,7 @@ export function AppSidebar({ onSelectChannel={onSelectChannel} onToggleCollapsed={() => toggleCollapsedGroup("channels")} selectedChannelId={selectedChannelId} - title="Channels" + title={t("nav.channels")} unreadChannelCounts={unreadChannelCounts} unreadChannelIds={unreadChannelIds} sections={channelSections} @@ -772,7 +774,7 @@ export function AppSidebar({ 0} isCollapsed={collapsedGroups.forums} isActiveChannel={selectedView === "channel"} @@ -791,7 +793,7 @@ export function AppSidebar({ onSelectChannel={onSelectChannel} onToggleCollapsed={() => toggleCollapsedGroup("forums")} selectedChannelId={selectedChannelId} - title="Forums" + title={t("nav.forums")} unreadChannelCounts={unreadChannelCounts} unreadChannelIds={unreadChannelIds} mutedChannelIds={mutedChannelIds} @@ -804,12 +806,12 @@ export function AppSidebar({ action={
- Inbox + {t("nav.inbox")} {homeBadgeCount > 0 ? ( - Pulse + {t("nav.pulse")} @@ -135,11 +137,11 @@ export function AppSidebarPrimaryMenu({ data-testid="open-projects-view" isActive={selectedView === "projects"} onClick={onSelectProjects} - tooltip="Projects" + tooltip={t("nav.projects")} type="button" > - Projects + {t("nav.projects")} @@ -148,11 +150,11 @@ export function AppSidebarPrimaryMenu({ data-testid="open-agents-view" isActive={selectedView === "agents"} onClick={onSelectAgents} - tooltip="Agents" + tooltip={t("nav.agents")} type="button" > - Agents + {t("nav.agents")} @@ -161,11 +163,11 @@ export function AppSidebarPrimaryMenu({ data-testid="open-workflows-view" isActive={selectedView === "workflows"} onClick={onSelectWorkflows} - tooltip="Workflows" + tooltip={t("nav.workflows")} type="button" > - Workflows + {t("nav.workflows")} diff --git a/desktop/src/main.tsx b/desktop/src/main.tsx index 890ef72226..ad181ffb74 100644 --- a/desktop/src/main.tsx +++ b/desktop/src/main.tsx @@ -8,6 +8,7 @@ import { UpdaterProvider } from "@/features/settings/hooks/UpdaterProvider"; import { migrateLegacyCommunityStorageBeforeRender } from "@/features/communities/legacyCommunityStorage"; import { CommunitiesProvider } from "@/features/communities/useCommunities"; import { CommunityOnboardingProvider } from "@/features/onboarding/communityOnboarding"; +import { I18nProvider } from "@/shared/i18n"; import { ThemeProvider } from "@/shared/theme/ThemeProvider"; import { EmojiBurstProvider } from "@/shared/ui/EmojiBurstProvider"; import { PoofBurstProvider } from "@/shared/ui/PoofBurstProvider"; @@ -76,17 +77,19 @@ function renderApp() { - - - - - - - - - - - + + + + + + + + + + + + + diff --git a/desktop/src/shared/i18n/I18nProvider.tsx b/desktop/src/shared/i18n/I18nProvider.tsx new file mode 100644 index 0000000000..2fe51f3041 --- /dev/null +++ b/desktop/src/shared/i18n/I18nProvider.tsx @@ -0,0 +1,98 @@ +import { + type ReactNode, + createContext, + useCallback, + useContext, + useMemo, + useState, + useEffect, +} from "react"; + +import { + DEFAULT_LANG, + LANG_STORAGE_KEY, + type Lang, + type MsgKey, + isLang, + translate, +} from "./messages"; + +type I18nContextValue = { + lang: Lang; + setLang: (lang: Lang) => void; + t: (key: MsgKey, vars?: Record) => string; +}; + +const I18nContext = createContext(undefined); + +function readStoredLang(): Lang { + try { + const stored = window.localStorage.getItem(LANG_STORAGE_KEY); + if (isLang(stored)) { + return stored; + } + } catch { + // ignore quota / private mode + } + return DEFAULT_LANG; +} + +function applyDocumentLang(lang: Lang) { + document.documentElement.lang = lang === "zh" ? "zh-CN" : "en"; +} + +export function I18nProvider({ children }: { children: ReactNode }) { + const [lang, setLangState] = useState(() => readStoredLang()); + + useEffect(() => { + applyDocumentLang(lang); + }, [lang]); + + const setLang = useCallback((next: Lang) => { + setLangState(next); + try { + window.localStorage.setItem(LANG_STORAGE_KEY, next); + } catch { + // ignore + } + applyDocumentLang(next); + }, []); + + const t = useCallback( + (key: MsgKey, vars?: Record) => + translate(key, lang, vars), + [lang], + ); + + const value = useMemo( + () => ({ + lang, + setLang, + t, + }), + [lang, setLang, t], + ); + + return {children}; +} + +export function useI18n(): I18nContextValue { + const ctx = useContext(I18nContext); + if (!ctx) { + throw new Error("useI18n must be used within I18nProvider"); + } + return ctx; +} + +/** Safe for optional trees (tests / isolated previews). Falls back to default zh. */ +export function useOptionalI18n(): I18nContextValue { + const ctx = useContext(I18nContext); + if (ctx) { + return ctx; + } + return { + lang: DEFAULT_LANG, + setLang: () => {}, + t: (key, vars) => translate(key, DEFAULT_LANG, vars), + }; +} diff --git a/desktop/src/shared/i18n/LanguageToggle.tsx b/desktop/src/shared/i18n/LanguageToggle.tsx new file mode 100644 index 0000000000..18b9dc1632 --- /dev/null +++ b/desktop/src/shared/i18n/LanguageToggle.tsx @@ -0,0 +1,83 @@ +import { cn } from "@/shared/lib/cn"; +import { useI18n } from "./I18nProvider"; +import type { Lang } from "./messages"; + +type LanguageToggleProps = { + className?: string; + /** compact: top chrome EN|中文; full: settings card radios */ + variant?: "compact" | "full"; + testId?: string; +}; + +const OPTIONS: Array<{ value: Lang; compactKey: "chrome.lang.en" | "chrome.lang.zh"; fullKey: "appearance.language.en" | "appearance.language.zh" }> = + [ + { value: "zh", compactKey: "chrome.lang.zh", fullKey: "appearance.language.zh" }, + { value: "en", compactKey: "chrome.lang.en", fullKey: "appearance.language.en" }, + ]; + +export function LanguageToggle({ + className, + variant = "compact", + testId = "language-toggle", +}: LanguageToggleProps) { + const { lang, setLang, t } = useI18n(); + + if (variant === "full") { + return ( +
+ {OPTIONS.map((opt) => ( + + ))} +
+ ); + } + + return ( +
+ {OPTIONS.map((opt) => ( + + ))} +
+ ); +} diff --git a/desktop/src/shared/i18n/i18n.test.mjs b/desktop/src/shared/i18n/i18n.test.mjs new file mode 100644 index 0000000000..7233692f98 --- /dev/null +++ b/desktop/src/shared/i18n/i18n.test.mjs @@ -0,0 +1,42 @@ +import assert from "node:assert/strict"; +import { describe, it } from "node:test"; + +import { + DEFAULT_LANG, + isLang, + messages, + translate, +} from "./messages.ts"; + +describe("desktop i18n", () => { + it("defaults to Chinese", () => { + assert.equal(DEFAULT_LANG, "zh"); + }); + + it("recognizes supported langs only", () => { + assert.equal(isLang("zh"), true); + assert.equal(isLang("en"), true); + assert.equal(isLang("fr"), false); + assert.equal(isLang(null), false); + }); + + it("has matching keys in en and zh", () => { + const enKeys = Object.keys(messages.en).sort(); + const zhKeys = Object.keys(messages.zh).sort(); + assert.deepEqual(enKeys, zhKeys); + }); + + it("translates main-path keys", () => { + assert.equal(translate("nav.inbox", "en"), "Inbox"); + assert.equal(translate("nav.inbox", "zh"), "收件箱"); + assert.equal(translate("nav.channels", "zh"), "频道"); + assert.equal(translate("appearance.language.title", "zh"), "语言"); + }); + + it("falls back to English for missing zh (defensive)", () => { + // All keys exist; unknown key returns the key itself via ?? chain + // when accessed incorrectly — translate uses typed keys only. + assert.equal(translate("common.save", "en"), "Save"); + assert.equal(translate("common.save", "zh"), "保存"); + }); +}); diff --git a/desktop/src/shared/i18n/index.ts b/desktop/src/shared/i18n/index.ts new file mode 100644 index 0000000000..4946a2f08f --- /dev/null +++ b/desktop/src/shared/i18n/index.ts @@ -0,0 +1,11 @@ +export { + DEFAULT_LANG, + LANG_STORAGE_KEY, + type Lang, + type MsgKey, + isLang, + messages, + translate, +} from "./messages"; +export { I18nProvider, useI18n, useOptionalI18n } from "./I18nProvider"; +export { LanguageToggle } from "./LanguageToggle"; diff --git a/desktop/src/shared/i18n/messages.ts b/desktop/src/shared/i18n/messages.ts new file mode 100644 index 0000000000..9fa843a1f0 --- /dev/null +++ b/desktop/src/shared/i18n/messages.ts @@ -0,0 +1,207 @@ +export type Lang = "en" | "zh"; + +export const LANG_STORAGE_KEY = "buzz-desktop-lang"; + +/** Default per product decision: Chinese. */ +export const DEFAULT_LANG: Lang = "zh"; + +const en = { + // Top chrome + "chrome.toggleSidebar": "Toggle Sidebar", + "chrome.back": "Go back", + "chrome.forward": "Go forward", + "chrome.language": "Language", + "chrome.lang.en": "EN", + "chrome.lang.zh": "中文", + + // Sidebar primary + "nav.inbox": "Inbox", + "nav.pulse": "Pulse", + "nav.projects": "Projects", + "nav.agents": "Agents", + "nav.workflows": "Workflows", + "nav.channels": "Channels", + "nav.forums": "Forums", + "nav.directMessages": "Direct messages", + "nav.starred": "Starred", + "nav.settings": "Settings", + "nav.browseChannels": "Browse channels", + "nav.newForum": "New forum", + "nav.newMessage": "New message", + "nav.createChannel": "Create channel", + "nav.sectionDirectMessages": "direct messages", + + // Search + "search.placeholder": "Search", + "search.everything": "Search everything", + "search.channels": "Channels", + "search.directMessages": "Direct messages", + "search.users": "People", + "search.agents": "Agents", + "search.mostRelevant": "Most relevant", + "search.actions": "Actions", + "search.browseChannels": "Browse channels", + "search.createChannel": "Create a new channel", + "search.createAgent": "Create a new agent", + "search.directMessage": "Direct message", + "search.noResults": "No results", + "search.noMatches": "No matches for", + + // Settings nav groups + "settings.group.personal": "Personal", + "settings.group.communities": "Communities", + "settings.group.app": "App", + "settings.back": "Back", + "settings.title": "Settings", + + // Settings sections + "settings.section.appearance": "Appearance", + "settings.section.profile": "Profile", + "settings.section.notifications": "Notifications", + "settings.section.experimental": "Experiments", + "settings.section.agents": "Agents", + "settings.section.channel-templates": "Templates", + "settings.section.compute": "Compute", + "settings.section.shortcuts": "Shortcuts", + "settings.section.hosted-communities": "Hosted communities", + "settings.section.community-members": "Invites", + "settings.section.moderation": "Moderation", + "settings.section.custom-emoji": "Custom emoji", + "settings.section.local-archive": "Local archive", + "settings.section.mobile": "Mobile", + "settings.section.updates": "Updates", + + // Appearance + "appearance.title": "Appearance", + "appearance.description": "Choose a theme for Buzz.", + "appearance.mode.system": "System", + "appearance.mode.light": "Light", + "appearance.mode.dark": "Dark", + "appearance.language.title": "Language", + "appearance.language.description": + "Interface language for the desktop app. Saved on this device.", + "appearance.language.zh": "中文", + "appearance.language.en": "English", + + // Common actions + "common.retry": "Retry", + "common.cancel": "Cancel", + "common.save": "Save", + "common.close": "Close", + "common.create": "Create", + "common.delete": "Delete", + "common.loading": "Loading…", + "common.empty": "Nothing here yet", + "common.signOut": "Sign out", + "common.markAsRead": "Mark as read", + "common.error": "Something went wrong", +} as const; + +export type MsgKey = keyof typeof en; + +const zh: Record = { + "chrome.toggleSidebar": "切换侧栏", + "chrome.back": "后退", + "chrome.forward": "前进", + "chrome.language": "语言", + "chrome.lang.en": "EN", + "chrome.lang.zh": "中文", + + "nav.inbox": "收件箱", + "nav.pulse": "动态", + "nav.projects": "项目", + "nav.agents": "智能体", + "nav.workflows": "工作流", + "nav.channels": "频道", + "nav.forums": "论坛", + "nav.directMessages": "私信", + "nav.starred": "已加星标", + "nav.settings": "设置", + "nav.browseChannels": "浏览频道", + "nav.newForum": "新建论坛", + "nav.newMessage": "新消息", + "nav.createChannel": "创建频道", + "nav.sectionDirectMessages": "私信", + + "search.placeholder": "搜索", + "search.everything": "搜索全部", + "search.channels": "频道", + "search.directMessages": "私信", + "search.users": "用户", + "search.agents": "智能体", + "search.mostRelevant": "最相关", + "search.actions": "操作", + "search.browseChannels": "浏览频道", + "search.createChannel": "创建新频道", + "search.createAgent": "创建新智能体", + "search.directMessage": "私信", + "search.noResults": "无结果", + "search.noMatches": "无匹配:", + + "settings.group.personal": "个人", + "settings.group.communities": "社区", + "settings.group.app": "应用", + "settings.back": "返回", + "settings.title": "设置", + + "settings.section.appearance": "外观", + "settings.section.profile": "个人资料", + "settings.section.notifications": "通知", + "settings.section.experimental": "实验功能", + "settings.section.agents": "智能体", + "settings.section.channel-templates": "模板", + "settings.section.compute": "算力", + "settings.section.shortcuts": "快捷键", + "settings.section.hosted-communities": "托管社区", + "settings.section.community-members": "邀请", + "settings.section.moderation": "审核", + "settings.section.custom-emoji": "自定义表情", + "settings.section.local-archive": "本地归档", + "settings.section.mobile": "移动端", + "settings.section.updates": "更新", + + "appearance.title": "外观", + "appearance.description": "为 Buzz 选择主题。", + "appearance.mode.system": "跟随系统", + "appearance.mode.light": "浅色", + "appearance.mode.dark": "深色", + "appearance.language.title": "语言", + "appearance.language.description": "桌面端界面语言,保存在本机。", + "appearance.language.zh": "中文", + "appearance.language.en": "English", + + "common.retry": "重试", + "common.cancel": "取消", + "common.save": "保存", + "common.close": "关闭", + "common.create": "创建", + "common.delete": "删除", + "common.loading": "加载中…", + "common.empty": "暂无内容", + "common.signOut": "退出登录", + "common.markAsRead": "标为已读", + "common.error": "出错了", +}; + +export const messages: Record> = { + en: en as Record, + zh, +}; + +export function isLang(value: unknown): value is Lang { + return value === "en" || value === "zh"; +} + +export function translate( + key: MsgKey, + lang: Lang, + vars?: Record, +): string { + let text = messages[lang][key] ?? messages.en[key] ?? key; + if (vars) { + for (const [name, value] of Object.entries(vars)) { + text = text.replaceAll(`{${name}}`, String(value)); + } + } + return text; +} diff --git a/desktop/src/shared/styles/globals.css b/desktop/src/shared/styles/globals.css index fd60621933..c3c09cfcb9 100644 --- a/desktop/src/shared/styles/globals.css +++ b/desktop/src/shared/styles/globals.css @@ -1,3 +1,4 @@ +@import url("https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@500;600;700;800&display=swap"); @import "tailwindcss"; @import "tw-animate-css"; @import "./globals/scrollbars.css"; diff --git a/desktop/src/shared/styles/globals/theme.css b/desktop/src/shared/styles/globals/theme.css index 0810a15f0a..42506d3d24 100644 --- a/desktop/src/shared/styles/globals/theme.css +++ b/desktop/src/shared/styles/globals/theme.css @@ -1,19 +1,19 @@ @layer base { :root { - /* Catppuccin Latte (mauve accent) */ - --radius: 0.625rem; - --background: 220 23.08% 94.9%; - --foreground: 234 16.02% 35.49%; - --card: 220 23.08% 94.9%; - --card-foreground: 234 16.02% 35.49%; - --popover: 220 23.08% 94.9%; - --popover-foreground: 234 16.02% 35.49%; - --primary: 266 85.05% 58.04%; - --primary-foreground: 220 23.08% 94.9%; - --secondary: 223 15.91% 82.75%; - --secondary-foreground: 234 16.02% 35.49%; - --muted: 223 15.91% 82.75%; - --muted-foreground: 233 12.8% 41.37%; + /* BC home_portal "raft" theme — cream paper + charcoal side + amber */ + --radius: 0rem; + --background: 30 100% 97%; + --foreground: 24 10% 10%; + --card: 0 0% 100%; + --card-foreground: 24 10% 10%; + --popover: 0 0% 100%; + --popover-foreground: 24 10% 10%; + --primary: 43 96% 56%; + --primary-foreground: 24 10% 10%; + --secondary: 30 25% 92%; + --secondary-foreground: 24 10% 10%; + --muted: 30 20% 90%; + --muted-foreground: 30 6% 32%; --huddle-drawer-surface: 0 0% 0%; --huddle-control-surface: 0 0% 20%; --huddle-control-hover-surface: 0 0% 24%; @@ -24,34 +24,34 @@ --huddle-popover-border: 0 0% 24%; --huddle-tooltip-surface: 0 0% 20%; --huddle-tooltip-foreground: 0 0% 98%; - --accent: 223 15.91% 82.75%; - --accent-foreground: 234 16.02% 35.49%; - --destructive: 347 86.67% 44.12%; - --destructive-foreground: 220 23.08% 94.9%; - --border: 225 13.56% 76.86%; - --input: 225 13.56% 76.86%; - --ring: 234 16.02% 35.49%; - --chart-1: 345 57.65% 74.12%; - --chart-2: 185 37.25% 70.59%; - --chart-3: 219 81.96% 78.43%; - --chart-4: 35 61.18% 76.86%; - --chart-5: 113 32.94% 73.33%; - --sidebar: 218 25.1% 93.73%; - --sidebar-background: 218 25.1% 93.73%; - --sidebar-foreground: 234 16.02% 35.49%; - --sidebar-primary: 266 85.05% 58.04%; - --sidebar-primary-foreground: 220 23.08% 94.9%; + --accent: 43 96% 90%; + --accent-foreground: 24 10% 10%; + --destructive: 347 77% 50%; + --destructive-foreground: 0 0% 100%; + --border: 24 10% 10%; + --input: 24 10% 10%; + --ring: 43 96% 56%; + --chart-1: 43 96% 56%; + --chart-2: 330 80% 75%; + --chart-3: 142 50% 40%; + --chart-4: 24 10% 30%; + --chart-5: 30 6% 45%; + --sidebar: 24 10% 15%; + --sidebar-background: 24 10% 15%; + --sidebar-foreground: 60 9% 98%; + --sidebar-primary: 43 96% 56%; + --sidebar-primary-foreground: 24 10% 10%; --sidebar-active: var(--sidebar-primary); --sidebar-active-foreground: var(--sidebar-primary-foreground); - --sidebar-accent: 220 23.08% 94.9%; - --sidebar-accent-foreground: 234 16.02% 35.49%; - --sidebar-border: 225 13.56% 76.86%; - --sidebar-ring: 225 13.56% 76.86%; + --sidebar-accent: 24 10% 20%; + --sidebar-accent-foreground: 60 9% 98%; + --sidebar-border: 24 6% 25%; + --sidebar-ring: 43 96% 56%; /* Exact Buzz palette tokens; components consume these semantically. */ - --buzz-gradient-light-top: #e6e6b6; - --buzz-gradient-light-bottom: #c4d0da; - --buzz-gradient-dark-top: #4a4616; - --buzz-gradient-dark-bottom: #0a1423; + --buzz-gradient-light-top: #fff8f0; + --buzz-gradient-light-bottom: #f5f0e8; + --buzz-gradient-dark-top: #292524; + --buzz-gradient-dark-bottom: #1c1917; --buzz-content-dark: 0 0% 10.1960784314%; /* Hosted-community onboarding intentionally keeps this light palette in both app themes; semantic tokens keep component code color-agnostic. */ @@ -66,20 +66,20 @@ } .dark { - /* Catppuccin Macchiato (mauve accent) */ - --radius: 0.625rem; - --background: 232 23.4% 18.43%; - --foreground: 227 68.25% 87.65%; - --card: 232 23.4% 18.43%; - --card-foreground: 227 68.25% 87.65%; - --popover: 232 23.4% 18.43%; - --popover-foreground: 227 68.25% 87.65%; - --primary: 267 82.69% 79.61%; - --primary-foreground: 232 23.4% 18.43%; - --secondary: 230 18.8% 26.08%; - --secondary-foreground: 227 68.25% 87.65%; - --muted: 230 18.8% 26.08%; - --muted-foreground: 228 39.22% 80%; + /* BC home "ruff" dark — warm stone + amber */ + --radius: 0rem; + --background: 24 10% 10%; + --foreground: 60 9% 98%; + --card: 24 10% 15%; + --card-foreground: 60 9% 98%; + --popover: 24 10% 15%; + --popover-foreground: 60 9% 98%; + --primary: 43 96% 56%; + --primary-foreground: 24 10% 10%; + --secondary: 24 6% 20%; + --secondary-foreground: 60 9% 98%; + --muted: 24 6% 20%; + --muted-foreground: 30 6% 65%; --huddle-drawer-surface: 230 18.8% 26.08%; --huddle-control-surface: 231 15.61% 33.92%; --huddle-control-hover-surface: 231 15.61% 38%; @@ -90,35 +90,40 @@ --huddle-popover-border: 231 15.61% 38%; --huddle-tooltip-surface: 231 15.61% 33.92%; --huddle-tooltip-foreground: 227 68.25% 87.65%; - --accent: 230 18.8% 26.08%; - --accent-foreground: 227 68.25% 87.65%; - --destructive: 351 73.91% 72.94%; - --destructive-foreground: 232 23.4% 18.43%; - --border: 231 15.61% 33.92%; - --input: 231 15.61% 33.92%; - --ring: 227 68.25% 87.65%; - --chart-1: 333 20.78% 38.04%; - --chart-2: 193 19.61% 38.04%; - --chart-3: 224 26.67% 41.18%; - --chart-4: 30 8.24% 42.35%; - --chart-5: 139 10.98% 38.82%; - --sidebar: 234 22.75% 17.25%; - --sidebar-background: 234 22.75% 17.25%; - --sidebar-foreground: 227 68.25% 87.65%; - --sidebar-primary: 267 82.69% 79.61%; - --sidebar-primary-foreground: 232 23.4% 18.43%; + --accent: 24 6% 22%; + --accent-foreground: 60 9% 98%; + --destructive: 0 72% 55%; + --destructive-foreground: 0 0% 100%; + --border: 24 6% 28%; + --input: 24 6% 28%; + --ring: 43 96% 56%; + --chart-1: 43 96% 56%; + --chart-2: 330 70% 65%; + --chart-3: 142 50% 45%; + --chart-4: 30 6% 50%; + --chart-5: 24 10% 40%; + --sidebar: 24 10% 12%; + --sidebar-background: 24 10% 12%; + --sidebar-foreground: 60 9% 98%; + --sidebar-primary: 43 96% 56%; + --sidebar-primary-foreground: 24 10% 10%; --sidebar-active: var(--sidebar-primary); --sidebar-active-foreground: var(--sidebar-primary-foreground); - --sidebar-accent: 232 23.4% 18.43%; - --sidebar-accent-foreground: 227 68.25% 87.65%; - --sidebar-border: 231 15.61% 33.92%; - --sidebar-ring: 231 15.61% 33.92%; + --sidebar-accent: 24 8% 18%; + --sidebar-accent-foreground: 60 9% 98%; + --sidebar-border: 24 6% 22%; + --sidebar-ring: 43 96% 56%; } * { @apply border-border; } + body { + font-family: "Space Grotesk", "Segoe UI", "PingFang SC", "Helvetica Neue", sans-serif; + letter-spacing: -0.01em; + } + html, body, #root { diff --git a/desktop/src/shared/ui/button.tsx b/desktop/src/shared/ui/button.tsx index 70e2c0a045..036cbb7d62 100644 --- a/desktop/src/shared/ui/button.tsx +++ b/desktop/src/shared/ui/button.tsx @@ -10,12 +10,13 @@ const buttonVariants = cva( variants: { variant: { default: - "bg-primary text-primary-foreground shadow hover:bg-primary/90", + "border-2 border-foreground/90 bg-primary text-primary-foreground shadow-none hover:bg-primary/90", destructive: - "bg-destructive text-destructive-foreground shadow-xs hover:bg-destructive/90", - outline: "border border-input/40 bg-background hover:bg-muted/70", + "border-2 border-destructive bg-destructive text-destructive-foreground shadow-xs hover:bg-destructive/90", + outline: + "border-2 border-foreground/80 bg-background hover:bg-muted/70", secondary: - "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80", + "border-2 border-border bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80", ghost: "hover:bg-accent hover:text-accent-foreground", link: "text-primary underline-offset-4 hover:underline", }, diff --git a/desktop/src/shared/ui/input.tsx b/desktop/src/shared/ui/input.tsx index 3983bc149f..a5236f45d0 100644 --- a/desktop/src/shared/ui/input.tsx +++ b/desktop/src/shared/ui/input.tsx @@ -11,7 +11,7 @@ const Input = React.forwardRef>( spellCheck={false} type={type} className={cn( - "flex h-9 w-full rounded-lg border border-input/40 bg-background px-3 py-1 text-base transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-hidden focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm", + "flex h-9 w-full rounded-lg border-2 border-input/70 bg-background px-3 py-1 text-base transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-hidden focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm", className, )} ref={ref} diff --git a/desktop/tailwind.config.js b/desktop/tailwind.config.js index 8905fc16ce..da14de5b68 100644 --- a/desktop/tailwind.config.js +++ b/desktop/tailwind.config.js @@ -39,9 +39,10 @@ export default { }, fontFamily: { sans: [ + '"Space Grotesk"', '"Inter Variable"', "Inter", - '"Avenir Next"', + '"PingFang SC"', '"Segoe UI"', "sans-serif", ], diff --git a/preview-ui/index.html b/preview-ui/index.html new file mode 100644 index 0000000000..2f03866124 --- /dev/null +++ b/preview-ui/index.html @@ -0,0 +1,246 @@ + + + + + + Buzz UI Preview · BC home / Raft theme + + + + + +
+ +
+
+

#WORK总控 · UI preview

+
+ + + +
+
+ +
+
+
+
+
@Cindy
+
Theme aligned to BC home: cream paper, charcoal sidebar, amber accent, hard borders.
+
+
+
You
+
Also want Chinese / English switch.
+
+
+
@Cindy
+
Language toggle is live in this preview (top-right EN / 中文).
+
+
+
+ + +
+
+ +
+
+
+ + + diff --git a/web/index.html b/web/index.html index 0b9f358282..ecf5c60314 100644 --- a/web/index.html +++ b/web/index.html @@ -4,7 +4,9 @@ Buzz - + + +
diff --git a/web/src/shared/styles/globals.css b/web/src/shared/styles/globals.css index e7255a9cb7..1de832b32c 100644 --- a/web/src/shared/styles/globals.css +++ b/web/src/shared/styles/globals.css @@ -1,83 +1,84 @@ @import "tailwindcss"; +@import url("https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@500;600;700;800&display=swap"); @import "tw-animate-css"; @plugin "@tailwindcss/typography"; @config "../../../tailwind.config.js"; @layer base { :root { - /* Catppuccin Latte (mauve accent) */ - --radius: 0.625rem; - --background: 220 23.08% 94.9%; - --foreground: 234 16.02% 35.49%; - --card: 220 23.08% 94.9%; - --card-foreground: 234 16.02% 35.49%; - --popover: 220 23.08% 94.9%; - --popover-foreground: 234 16.02% 35.49%; - --primary: 266 85.05% 58.04%; - --primary-foreground: 220 23.08% 94.9%; - --secondary: 223 15.91% 82.75%; - --secondary-foreground: 234 16.02% 35.49%; - --muted: 223 15.91% 82.75%; - --muted-foreground: 233 12.8% 41.37%; - --accent: 223 15.91% 82.75%; - --accent-foreground: 234 16.02% 35.49%; - --destructive: 347 86.67% 44.12%; - --destructive-foreground: 220 23.08% 94.9%; - --border: 225 13.56% 76.86%; - --input: 225 13.56% 76.86%; - --ring: 234 16.02% 35.49%; - --chart-1: 345 57.65% 74.12%; - --chart-2: 185 37.25% 70.59%; - --chart-3: 219 81.96% 78.43%; - --chart-4: 35 61.18% 76.86%; - --chart-5: 113 32.94% 73.33%; - --sidebar: 218 25.1% 93.73%; - --sidebar-background: 218 25.1% 93.73%; - --sidebar-foreground: 234 16.02% 35.49%; - --sidebar-primary: 266 85.05% 58.04%; - --sidebar-primary-foreground: 220 23.08% 94.9%; - --sidebar-accent: 220 23.08% 94.9%; - --sidebar-accent-foreground: 234 16.02% 35.49%; - --sidebar-border: 225 13.56% 76.86%; - --sidebar-ring: 225 13.56% 76.86%; + /* BC home_portal "raft" — cream + charcoal + amber */ + --radius: 0rem; + --background: 30 100% 97%; + --foreground: 24 10% 10%; + --card: 0 0% 100%; + --card-foreground: 24 10% 10%; + --popover: 0 0% 100%; + --popover-foreground: 24 10% 10%; + --primary: 43 96% 56%; + --primary-foreground: 24 10% 10%; + --secondary: 30 25% 92%; + --secondary-foreground: 24 10% 10%; + --muted: 30 20% 90%; + --muted-foreground: 30 6% 32%; + --accent: 43 96% 90%; + --accent-foreground: 24 10% 10%; + --destructive: 347 77% 50%; + --destructive-foreground: 0 0% 100%; + --border: 24 10% 10%; + --input: 24 10% 10%; + --ring: 43 96% 56%; + --chart-1: 43 96% 56%; + --chart-2: 330 80% 75%; + --chart-3: 142 50% 40%; + --chart-4: 24 10% 30%; + --chart-5: 30 6% 45%; + --sidebar: 24 10% 15%; + --sidebar-background: 24 10% 15%; + --sidebar-foreground: 60 9% 98%; + --sidebar-primary: 43 96% 56%; + --sidebar-primary-foreground: 24 10% 10%; + --sidebar-accent: 24 10% 20%; + --sidebar-accent-foreground: 60 9% 98%; + --sidebar-border: 24 6% 25%; + --sidebar-ring: 43 96% 56%; } .dark { - /* Catppuccin Macchiato (mauve accent) */ - --radius: 0.625rem; - --background: 232 23.4% 18.43%; - --foreground: 227 68.25% 87.65%; - --card: 232 23.4% 18.43%; - --card-foreground: 227 68.25% 87.65%; - --popover: 232 23.4% 18.43%; - --popover-foreground: 227 68.25% 87.65%; - --primary: 267 82.69% 79.61%; - --primary-foreground: 232 23.4% 18.43%; - --secondary: 230 18.8% 26.08%; - --secondary-foreground: 227 68.25% 87.65%; - --muted: 230 18.8% 26.08%; - --muted-foreground: 228 39.22% 80%; - --accent: 230 18.8% 26.08%; - --accent-foreground: 227 68.25% 87.65%; - --destructive: 351 73.91% 72.94%; - --destructive-foreground: 232 23.4% 18.43%; - --border: 231 15.61% 33.92%; - --input: 231 15.61% 33.92%; - --ring: 227 68.25% 87.65%; - --chart-1: 333 20.78% 38.04%; - --chart-2: 193 19.61% 38.04%; - --chart-3: 224 26.67% 41.18%; - --chart-4: 30 8.24% 42.35%; - --chart-5: 139 10.98% 38.82%; - --sidebar: 234 22.75% 17.25%; - --sidebar-background: 234 22.75% 17.25%; - --sidebar-foreground: 227 68.25% 87.65%; - --sidebar-primary: 267 82.69% 79.61%; - --sidebar-primary-foreground: 232 23.4% 18.43%; - --sidebar-accent: 232 23.4% 18.43%; - --sidebar-accent-foreground: 227 68.25% 87.65%; - --sidebar-border: 231 15.61% 33.92%; - --sidebar-ring: 231 15.61% 33.92%; + /* BC home ruff dark */ + --radius: 0rem; + --background: 24 10% 10%; + --foreground: 60 9% 98%; + --card: 24 10% 15%; + --card-foreground: 60 9% 98%; + --popover: 24 10% 15%; + --popover-foreground: 60 9% 98%; + --primary: 43 96% 56%; + --primary-foreground: 24 10% 10%; + --secondary: 24 6% 20%; + --secondary-foreground: 60 9% 98%; + --muted: 24 6% 20%; + --muted-foreground: 30 6% 65%; + --accent: 24 6% 22%; + --accent-foreground: 60 9% 98%; + --destructive: 0 72% 55%; + --destructive-foreground: 0 0% 100%; + --border: 24 6% 28%; + --input: 24 6% 28%; + --ring: 43 96% 56%; + --chart-1: 43 96% 56%; + --chart-2: 330 70% 65%; + --chart-3: 142 50% 45%; + --chart-4: 30 6% 50%; + --chart-5: 24 10% 40%; + --sidebar: 24 10% 12%; + --sidebar-background: 24 10% 12%; + --sidebar-foreground: 60 9% 98%; + --sidebar-primary: 43 96% 56%; + --sidebar-primary-foreground: 24 10% 10%; + --sidebar-accent: 24 8% 18%; + --sidebar-accent-foreground: 60 9% 98%; + --sidebar-border: 24 6% 22%; + --sidebar-ring: 43 96% 56%; } * { From db0a080b0f48e793d515cc9838a42a2d5fe34030 Mon Sep 17 00:00:00 2001 From: tianhei Date: Tue, 28 Jul 2026 21:06:26 +0800 Subject: [PATCH 2/2] fix(desktop): apply Raft tokens at runtime + main-path i18n Review needs_rework: ThemeProvider no longer lets createThemeVars / neutral accent wipe raft chrome on buzz/buzz-dark. Overlay raft shell vars after adaptive theme load, pin amber primary, and bump FOUC cache. Translate search empty/recent/actions/context and settings back/permissions/retry strings. Add raft-shell + i18n persistence unit tests. --- .../src/features/search/ui/TopbarSearch.tsx | 34 ++++--- .../features/settings/ui/SettingsPanels.tsx | 8 +- .../src/features/settings/ui/SettingsView.tsx | 13 ++- desktop/src/shared/i18n/I18nProvider.tsx | 26 ++---- desktop/src/shared/i18n/i18n.test.mjs | 41 ++++++++- desktop/src/shared/i18n/index.ts | 2 + desktop/src/shared/i18n/messages.ts | 56 ++++++++++++ desktop/src/shared/theme/ThemeProvider.tsx | 28 ++++-- desktop/src/shared/theme/raft-shell.test.mjs | 44 +++++++++ desktop/src/shared/theme/raft-shell.ts | 91 +++++++++++++++++++ .../src/shared/theme/useThemePreviewVars.ts | 9 +- 11 files changed, 296 insertions(+), 56 deletions(-) create mode 100644 desktop/src/shared/theme/raft-shell.test.mjs create mode 100644 desktop/src/shared/theme/raft-shell.ts diff --git a/desktop/src/features/search/ui/TopbarSearch.tsx b/desktop/src/features/search/ui/TopbarSearch.tsx index 34a7aaf192..83011e63fa 100644 --- a/desktop/src/features/search/ui/TopbarSearch.tsx +++ b/desktop/src/features/search/ui/TopbarSearch.tsx @@ -184,7 +184,8 @@ function getSearchHitChannelName( function getSearchHitContextLabel( hit: SearchHit, channelLookup: ReadonlyMap, - channelLabels?: Record, + channelLabels: Record | undefined, + t: (key: MsgKey) => string, ): SearchHitContextLabel { const channel = hit.channelId ? channelLookup.get(hit.channelId) : null; const channelName = getSearchHitChannelName( @@ -196,7 +197,7 @@ function getSearchHitContextLabel( if (channel?.channelType === "dm") { return { channelLabel: null, - text: "Direct message", // translated at call site when needed + text: t("search.directMessage"), }; } @@ -205,15 +206,15 @@ function getSearchHitContextLabel( return { channelLabel: channelName, text: channelName - ? `${isThread ? "Thread" : "Message"} in` + ? isThread + ? t("search.threadIn") + : t("search.messageIn") : isThread - ? "Thread" - : "Message", + ? t("search.thread") + : t("search.message"), }; } -// Keep English context chips for message hits in v1 — full coverage is phase 2. - function getResultSectionKey(result: SearchResult): SearchResultSectionKey { if (result.kind === "channel") { return result.channel.channelType === "dm" ? "direct-messages" : "channels"; @@ -649,7 +650,12 @@ export function TopbarSearch({ : null; const messageContextLabel = result.kind === "message" - ? getSearchHitContextLabel(result.hit, channelLookup, channelLabels) + ? getSearchHitContextLabel( + result.hit, + channelLookup, + channelLabels, + t, + ) : null; const title = result.kind === "channel" @@ -781,11 +787,11 @@ export function TopbarSearch({ const searchResultContent = isShowingSuggestions ? ( suggestionResults.length === 0 ? (
-

No recent activity yet.

+

{t("search.noRecentActivity")}

) : (
@@ -797,7 +803,7 @@ export function TopbarSearch({ {suggestedResults.length > 0 ? (
- Recent activity + {t("search.recentActivity")}
{suggestedResults.map((result) => renderSearchResultRow(result, resultIndex++), @@ -806,7 +812,9 @@ export function TopbarSearch({ ) : null} {suggestionActionResults.length > 0 ? (
-
Actions
+
+ {t("search.actions")} +
{suggestionActionResults.map((result) => renderSearchResultRow(result, resultIndex++), )} @@ -825,7 +833,7 @@ export function TopbarSearch({

) : searchableResults.length === 0 ? (

- No matches for {trimmedQuery}. + {t("search.noMatchesFor", { query: trimmedQuery })}

) : (
diff --git a/desktop/src/features/settings/ui/SettingsPanels.tsx b/desktop/src/features/settings/ui/SettingsPanels.tsx index 9c1302c718..8a323014f0 100644 --- a/desktop/src/features/settings/ui/SettingsPanels.tsx +++ b/desktop/src/features/settings/ui/SettingsPanels.tsx @@ -423,9 +423,9 @@ function ThemeSettingsCard() { setFollowSystem, } = useTheme(); - // Buzz themes pin a neutral accent (GitHub black in light, white in dark), - // so the accent picker is hidden while a Buzz theme is active. `themeName` is - // the effective theme, so this also covers System mode resolving to Buzz. + // Buzz (Raft) themes pin amber primary, so the accent picker is hidden while + // a Buzz theme is active. `themeName` is the effective theme, so this also + // covers System mode resolving to Buzz. const accentPickerHidden = isBuzzTheme(themeName); const shouldReduceMotion = useReducedMotion(); @@ -640,7 +640,7 @@ function ThemeSettingsCard() {
- {/* Accent color picker — hidden for Buzz themes (pinned neutral accent). + {/* Accent color picker — hidden for Buzz themes (pinned Raft amber). Reveal/hide with the translate-up + opacity fade defined by ACCENT_PICKER_TRANSITION above. Reduced motion skips the transition and just renders/unrenders. */} diff --git a/desktop/src/features/settings/ui/SettingsView.tsx b/desktop/src/features/settings/ui/SettingsView.tsx index 40db4d910e..f8ab3cd4f7 100644 --- a/desktop/src/features/settings/ui/SettingsView.tsx +++ b/desktop/src/features/settings/ui/SettingsView.tsx @@ -247,11 +247,11 @@ export function SettingsView({ - Back to app + {t("settings.backToApp")} @@ -264,7 +264,7 @@ export function SettingsView({ data-testid="community-access-loading" > - Checking invite permissions… + {t("settings.checkingInvitePermissions")}
) : null} {myMembershipQuery.isError ? ( @@ -274,7 +274,7 @@ export function SettingsView({ >
- Invite settings could not be checked. + {t("settings.inviteCheckFailed")}
) : null} @@ -292,8 +292,7 @@ export function SettingsView({ data-testid="community-access-snapshot-missing" > - Invite settings are unavailable. Relay recovery may still be in - progress. + {t("settings.inviteUnavailable")}
) : null} {visibleNavGroups.map((group) => { diff --git a/desktop/src/shared/i18n/I18nProvider.tsx b/desktop/src/shared/i18n/I18nProvider.tsx index 2fe51f3041..e4c1750fe5 100644 --- a/desktop/src/shared/i18n/I18nProvider.tsx +++ b/desktop/src/shared/i18n/I18nProvider.tsx @@ -10,10 +10,10 @@ import { import { DEFAULT_LANG, - LANG_STORAGE_KEY, type Lang, type MsgKey, - isLang, + loadStoredLang, + persistLang, translate, } from "./messages"; @@ -25,24 +25,14 @@ type I18nContextValue = { const I18nContext = createContext(undefined); -function readStoredLang(): Lang { - try { - const stored = window.localStorage.getItem(LANG_STORAGE_KEY); - if (isLang(stored)) { - return stored; - } - } catch { - // ignore quota / private mode - } - return DEFAULT_LANG; -} - function applyDocumentLang(lang: Lang) { document.documentElement.lang = lang === "zh" ? "zh-CN" : "en"; } export function I18nProvider({ children }: { children: ReactNode }) { - const [lang, setLangState] = useState(() => readStoredLang()); + const [lang, setLangState] = useState(() => + loadStoredLang(window.localStorage), + ); useEffect(() => { applyDocumentLang(lang); @@ -50,11 +40,7 @@ export function I18nProvider({ children }: { children: ReactNode }) { const setLang = useCallback((next: Lang) => { setLangState(next); - try { - window.localStorage.setItem(LANG_STORAGE_KEY, next); - } catch { - // ignore - } + persistLang(window.localStorage, next); applyDocumentLang(next); }, []); diff --git a/desktop/src/shared/i18n/i18n.test.mjs b/desktop/src/shared/i18n/i18n.test.mjs index 7233692f98..8a5561cd0d 100644 --- a/desktop/src/shared/i18n/i18n.test.mjs +++ b/desktop/src/shared/i18n/i18n.test.mjs @@ -3,8 +3,11 @@ import { describe, it } from "node:test"; import { DEFAULT_LANG, + LANG_STORAGE_KEY, isLang, + loadStoredLang, messages, + persistLang, translate, } from "./messages.ts"; @@ -31,12 +34,46 @@ describe("desktop i18n", () => { assert.equal(translate("nav.inbox", "zh"), "收件箱"); assert.equal(translate("nav.channels", "zh"), "频道"); assert.equal(translate("appearance.language.title", "zh"), "语言"); + assert.equal(translate("settings.backToApp", "zh"), "返回应用"); + assert.equal(translate("search.noRecentActivity", "en"), "No recent activity yet."); + assert.equal( + translate("search.noMatchesFor", "zh", { query: "foo" }), + "无匹配:foo。", + ); }); it("falls back to English for missing zh (defensive)", () => { - // All keys exist; unknown key returns the key itself via ?? chain - // when accessed incorrectly — translate uses typed keys only. assert.equal(translate("common.save", "en"), "Save"); assert.equal(translate("common.save", "zh"), "保存"); }); + + it("loadStoredLang defaults when empty and reads valid values", () => { + const store = new Map(); + const storage = { + getItem: (k) => (store.has(k) ? store.get(k) : null), + setItem: (k, v) => { + store.set(k, v); + }, + }; + assert.equal(loadStoredLang(storage), DEFAULT_LANG); + storage.setItem(LANG_STORAGE_KEY, "en"); + assert.equal(loadStoredLang(storage), "en"); + storage.setItem(LANG_STORAGE_KEY, "bogus"); + assert.equal(loadStoredLang(storage), DEFAULT_LANG); + }); + + it("persistLang writes storage so a later load keeps the choice", () => { + const store = new Map(); + const storage = { + getItem: (k) => (store.has(k) ? store.get(k) : null), + setItem: (k, v) => { + store.set(k, v); + }, + }; + assert.equal(persistLang(storage, "en"), true); + assert.equal(store.get(LANG_STORAGE_KEY), "en"); + assert.equal(loadStoredLang(storage), "en"); + assert.equal(persistLang(storage, "zh"), true); + assert.equal(loadStoredLang(storage), "zh"); + }); }); diff --git a/desktop/src/shared/i18n/index.ts b/desktop/src/shared/i18n/index.ts index 4946a2f08f..316416c244 100644 --- a/desktop/src/shared/i18n/index.ts +++ b/desktop/src/shared/i18n/index.ts @@ -4,7 +4,9 @@ export { type Lang, type MsgKey, isLang, + loadStoredLang, messages, + persistLang, translate, } from "./messages"; export { I18nProvider, useI18n, useOptionalI18n } from "./I18nProvider"; diff --git a/desktop/src/shared/i18n/messages.ts b/desktop/src/shared/i18n/messages.ts index 9fa843a1f0..15803b6dff 100644 --- a/desktop/src/shared/i18n/messages.ts +++ b/desktop/src/shared/i18n/messages.ts @@ -46,12 +46,25 @@ const en = { "search.directMessage": "Direct message", "search.noResults": "No results", "search.noMatches": "No matches for", + "search.noMatchesFor": "No matches for {query}.", + "search.noRecentActivity": "No recent activity yet.", + "search.recentActivity": "Recent activity", + "search.thread": "Thread", + "search.message": "Message", + "search.threadIn": "Thread in", + "search.messageIn": "Message in", // Settings nav groups "settings.group.personal": "Personal", "settings.group.communities": "Communities", "settings.group.app": "App", "settings.back": "Back", + "settings.backToApp": "Back to app", + "settings.checkingInvitePermissions": "Checking invite permissions…", + "settings.inviteCheckFailed": "Invite settings could not be checked.", + "settings.tryAgain": "Try again", + "settings.inviteUnavailable": + "Invite settings are unavailable. Relay recovery may still be in progress.", "settings.title": "Settings", // Settings sections @@ -137,11 +150,23 @@ const zh: Record = { "search.directMessage": "私信", "search.noResults": "无结果", "search.noMatches": "无匹配:", + "search.noMatchesFor": "无匹配:{query}。", + "search.noRecentActivity": "暂无最近活动。", + "search.recentActivity": "最近活动", + "search.thread": "帖子", + "search.message": "消息", + "search.threadIn": "帖子位于", + "search.messageIn": "消息位于", "settings.group.personal": "个人", "settings.group.communities": "社区", "settings.group.app": "应用", "settings.back": "返回", + "settings.backToApp": "返回应用", + "settings.checkingInvitePermissions": "正在检查邀请权限…", + "settings.inviteCheckFailed": "无法检查邀请设置。", + "settings.tryAgain": "重试", + "settings.inviteUnavailable": "邀请设置暂不可用。中继恢复可能仍在进行。", "settings.title": "设置", "settings.section.appearance": "外观", @@ -205,3 +230,34 @@ export function translate( } return text; } + +/** Read lang from a storage-like object (localStorage or test double). */ +export function loadStoredLang( + storage: Pick | null | undefined, +): Lang { + try { + const stored = storage?.getItem(LANG_STORAGE_KEY); + if (isLang(stored)) { + return stored; + } + } catch { + // ignore + } + return DEFAULT_LANG; +} + +/** Persist lang; returns false if storage write failed. */ +export function persistLang( + storage: Pick | null | undefined, + lang: Lang, +): boolean { + if (!isLang(lang)) { + return false; + } + try { + storage?.setItem(LANG_STORAGE_KEY, lang); + return true; + } catch { + return false; + } +} diff --git a/desktop/src/shared/theme/ThemeProvider.tsx b/desktop/src/shared/theme/ThemeProvider.tsx index d656815091..2e11c0fe57 100644 --- a/desktop/src/shared/theme/ThemeProvider.tsx +++ b/desktop/src/shared/theme/ThemeProvider.tsx @@ -12,6 +12,7 @@ import { getCurrentWindow } from "@tauri-apps/api/window"; import { invokeTauri } from "@/shared/api/tauri"; import { isMacPlatform } from "@/shared/lib/platform"; import { createThemeVars, hexToHsl } from "./adaptive-theme"; +import { RAFT_ACCENT_HEX, getRaftShellVars } from "./raft-shell"; import { SYNTAX_THEMES, type SyntaxThemeName, @@ -22,7 +23,8 @@ import { } from "./theme-loader"; export const THEME_STORAGE_KEY = "buzz-theme"; -const CACHE_KEY = "buzz-theme-cache"; +/** Bumped when Buzz shell palette changes so FOUC cache cannot pin stale GitHub-derived chrome. */ +const CACHE_KEY = "buzz-theme-cache.v2-raft"; export const ACCENT_STORAGE_KEY = "buzz-accent-color"; export const NEUTRAL_ACCENT = "neutral"; const FOLLOW_SYSTEM_KEY = "buzz-follow-system"; @@ -212,9 +214,9 @@ function applyAccentColor(value: string) { } /** - * The Buzz themes ship with a fixed neutral accent (the GitHub black/white - * foreground) rather than a user-selectable accent color. When a Buzz theme is - * active we force `NEUTRAL_ACCENT` regardless of the stored preference, and the + * The Buzz themes ship with a fixed Raft amber accent rather than a + * user-selectable accent color. When a Buzz theme is active we force + * {@link RAFT_ACCENT_HEX} regardless of the stored preference, and the * appearance panel hides the accent picker. The user's chosen accent is left * untouched in storage so it returns when they switch back to another theme. */ @@ -223,14 +225,14 @@ export function isBuzzTheme(themeName: string): boolean { } /** - * Resolve the accent to actually apply for a theme: Buzz themes are pinned to - * the neutral accent; every other theme uses the stored/selected accent. + * Resolve the accent to actually apply for a theme: Buzz (Raft) themes pin + * amber primary; every other theme uses the stored/selected accent. */ function resolveEffectiveAccent( themeName: string, accentColor: string, ): string { - return isBuzzTheme(themeName) ? NEUTRAL_ACCENT : accentColor; + return isBuzzTheme(themeName) ? RAFT_ACCENT_HEX : accentColor; } /** @@ -434,12 +436,20 @@ async function applyTheme( if (requestToken !== themeApplyRequest) return null; const info = extractThemeInfo(name, themeData); - const { isDark, vars } = createThemeVars(info.bg, info.fg, info.comment, { + let { isDark, vars } = createThemeVars(info.bg, info.fg, info.comment, { added: info.added, deleted: info.deleted, modified: info.modified, }); + // Buzz themes keep Shiki syntax colors from github-light/dark, but the app + // chrome must use Raft shell tokens (cream/charcoal/amber). Overlay after + // createThemeVars so inline styles do not pin the old GitHub-derived chrome. + if (isBuzzTheme(name)) { + isDark = name === "buzz-dark"; + vars = { ...vars, ...getRaftShellVars(isDark) }; + } + const root = document.documentElement; for (const [key, value] of Object.entries(vars)) { root.style.setProperty(key, value); @@ -459,7 +469,7 @@ async function applyTheme( // browser paints the new theme + accent together. Doing this in a later // microtask (e.g. the caller's `.then`) let the previous accent flash on the // new theme for a frame — the flicker seen when switching to Buzz. Buzz - // themes resolve to the neutral accent regardless of the stored value. + // (Raft) themes resolve to amber primary regardless of the stored value. applyAccentColor( resolveEffectiveAccent( name, diff --git a/desktop/src/shared/theme/raft-shell.test.mjs b/desktop/src/shared/theme/raft-shell.test.mjs new file mode 100644 index 0000000000..e871cf80e2 --- /dev/null +++ b/desktop/src/shared/theme/raft-shell.test.mjs @@ -0,0 +1,44 @@ +import assert from "node:assert/strict"; +import { describe, it } from "node:test"; + +import { + RAFT_ACCENT_HEX, + RAFT_LIGHT_BACKGROUND, + RAFT_LIGHT_PRIMARY, + RAFT_LIGHT_SIDEBAR, + getRaftShellVars, +} from "./raft-shell.ts"; + +describe("raft shell tokens", () => { + it("exports amber accent for Buzz runtime primary", () => { + assert.equal(RAFT_ACCENT_HEX, "#fbbf24"); + }); + + it("light shell matches cream paper / charcoal side / amber primary", () => { + const light = getRaftShellVars(false); + assert.equal(light["--background"], RAFT_LIGHT_BACKGROUND); + assert.equal(light["--primary"], RAFT_LIGHT_PRIMARY); + assert.equal(light["--sidebar-background"], RAFT_LIGHT_SIDEBAR); + assert.equal(light["--radius"], "0rem"); + // Cream-ish warm background (hue ~30, high lightness) + assert.match(light["--background"], /^30 /); + // Charcoal sidebar (low lightness) + assert.match(light["--sidebar-background"], /^24 /); + // Amber primary + assert.equal(light["--primary"], "43 96% 56%"); + }); + + it("dark shell keeps amber primary with warm stone surfaces", () => { + const dark = getRaftShellVars(true); + assert.equal(dark["--primary"], "43 96% 56%"); + assert.equal(dark["--background"], "24 10% 10%"); + assert.equal(dark["--sidebar-background"], "24 10% 12%"); + }); + + it("returns independent copies (no shared mutation)", () => { + const a = getRaftShellVars(false); + const b = getRaftShellVars(false); + a["--primary"] = "0 0% 0%"; + assert.equal(b["--primary"], "43 96% 56%"); + }); +}); diff --git a/desktop/src/shared/theme/raft-shell.ts b/desktop/src/shared/theme/raft-shell.ts new file mode 100644 index 0000000000..f974c2e530 --- /dev/null +++ b/desktop/src/shared/theme/raft-shell.ts @@ -0,0 +1,91 @@ +/** + * BC home_portal "Raft" shell tokens for the default Buzz themes. + * + * Syntax highlighting still loads via Shiki (github-light/dark), but the app + * chrome must use these values — ThemeProvider merges them after + * createThemeVars so inline styles do not wipe the Raft palette. + * + * Values mirror desktop/src/shared/styles/globals/theme.css (:root / .dark). + */ + +export type RaftShellVars = Record; + +/** Amber primary used by Raft (approx #fbbf24). */ +export const RAFT_ACCENT_HEX = "#fbbf24"; + +const RAFT_LIGHT: RaftShellVars = { + "--radius": "0rem", + "--background": "30 100% 97%", + "--foreground": "24 10% 10%", + "--card": "0 0% 100%", + "--card-foreground": "24 10% 10%", + "--popover": "0 0% 100%", + "--popover-foreground": "24 10% 10%", + "--primary": "43 96% 56%", + "--primary-foreground": "24 10% 10%", + "--secondary": "30 25% 92%", + "--secondary-foreground": "24 10% 10%", + "--muted": "30 20% 90%", + "--muted-foreground": "30 6% 32%", + "--accent": "43 96% 90%", + "--accent-foreground": "24 10% 10%", + "--destructive": "347 77% 50%", + "--destructive-foreground": "0 0% 100%", + "--border": "24 10% 10%", + "--input": "24 10% 10%", + "--ring": "43 96% 56%", + "--sidebar": "24 10% 15%", + "--sidebar-background": "24 10% 15%", + "--sidebar-foreground": "60 9% 98%", + "--sidebar-primary": "43 96% 56%", + "--sidebar-primary-foreground": "24 10% 10%", + "--sidebar-active": "43 96% 56%", + "--sidebar-active-foreground": "24 10% 10%", + "--sidebar-accent": "24 10% 20%", + "--sidebar-accent-foreground": "60 9% 98%", + "--sidebar-border": "24 6% 25%", + "--sidebar-ring": "43 96% 56%", +}; + +const RAFT_DARK: RaftShellVars = { + "--radius": "0rem", + "--background": "24 10% 10%", + "--foreground": "60 9% 98%", + "--card": "24 10% 15%", + "--card-foreground": "60 9% 98%", + "--popover": "24 10% 15%", + "--popover-foreground": "60 9% 98%", + "--primary": "43 96% 56%", + "--primary-foreground": "24 10% 10%", + "--secondary": "24 6% 20%", + "--secondary-foreground": "60 9% 98%", + "--muted": "24 6% 20%", + "--muted-foreground": "30 6% 65%", + "--accent": "24 6% 22%", + "--accent-foreground": "60 9% 98%", + "--destructive": "0 72% 55%", + "--destructive-foreground": "0 0% 100%", + "--border": "24 6% 28%", + "--input": "24 6% 28%", + "--ring": "43 96% 56%", + "--sidebar": "24 10% 12%", + "--sidebar-background": "24 10% 12%", + "--sidebar-foreground": "60 9% 98%", + "--sidebar-primary": "43 96% 56%", + "--sidebar-primary-foreground": "24 10% 10%", + "--sidebar-active": "43 96% 56%", + "--sidebar-active-foreground": "24 10% 10%", + "--sidebar-accent": "24 8% 18%", + "--sidebar-accent-foreground": "60 9% 98%", + "--sidebar-border": "24 6% 22%", + "--sidebar-ring": "43 96% 56%", +}; + +export function getRaftShellVars(isDark: boolean): RaftShellVars { + return isDark ? { ...RAFT_DARK } : { ...RAFT_LIGHT }; +} + +/** Expected computed-style samples for QA / unit checks (HSL channel triples). */ +export const RAFT_LIGHT_PRIMARY = RAFT_LIGHT["--primary"]; +export const RAFT_LIGHT_BACKGROUND = RAFT_LIGHT["--background"]; +export const RAFT_LIGHT_SIDEBAR = RAFT_LIGHT["--sidebar-background"]; diff --git a/desktop/src/shared/theme/useThemePreviewVars.ts b/desktop/src/shared/theme/useThemePreviewVars.ts index 2d2ab35652..67f7d88d52 100644 --- a/desktop/src/shared/theme/useThemePreviewVars.ts +++ b/desktop/src/shared/theme/useThemePreviewVars.ts @@ -14,6 +14,7 @@ import { } from "./ThemePreviewFrame"; import { NEUTRAL_ACCENT } from "./ThemeProvider"; import { hexToHsl } from "./adaptive-theme"; +import { getRaftShellVars } from "./raft-shell"; export type ThemePreviewVarsByTheme = Partial< Record @@ -25,11 +26,14 @@ let themePreviewVarsPromise: Promise | null = null; async function loadThemePreviewVars(name: SyntaxThemeName) { const themeData = await loadThemeData(name); const info = extractThemeInfo(name, themeData); - const { vars } = createThemeVars(info.bg, info.fg, info.comment, { + let { vars } = createThemeVars(info.bg, info.fg, info.comment, { added: info.added, deleted: info.deleted, modified: info.modified, }); + if (name === "buzz" || name === "buzz-dark") { + vars = { ...vars, ...getRaftShellVars(name === "buzz-dark") }; + } return [name, vars] as const; } @@ -86,6 +90,9 @@ export function useThemePreviewVars() { } export function getThemeFallbackPreviewVars(name: SyntaxThemeName) { + if (name === "buzz" || name === "buzz-dark") { + return getRaftShellVars(name === "buzz-dark"); + } return isLightTheme(name) ? LIGHT_PREVIEW_VARS : DARK_PREVIEW_VARS; }