feat(characters): add catalog boundary and capabilities

This commit is contained in:
2026-07-17 19:11:09 +08:00
parent a30e9937b8
commit 2fc312b5c7
9 changed files with 235 additions and 31 deletions
@@ -0,0 +1,36 @@
import { renderToStaticMarkup } from "react-dom/server";
import { describe, expect, it } from "vitest";
import { CHARACTERS } from "@/data/constants/character";
import {
CharacterCatalogProvider,
useCharacterCatalog,
} from "@/providers/character-catalog-provider";
describe("CharacterCatalogProvider", () => {
it("provides the complete local character catalog", () => {
const html = renderToStaticMarkup(
<CharacterCatalogProvider>
<CatalogProbe />
</CharacterCatalogProvider>,
);
expect(html).toContain("elio,maya,nayeli");
});
it("accepts a serializable catalog snapshot", () => {
const html = renderToStaticMarkup(
<CharacterCatalogProvider characters={[CHARACTERS[1]]}>
<CatalogProbe />
</CharacterCatalogProvider>,
);
expect(html).toContain("maya");
expect(html).not.toContain("elio");
});
});
function CatalogProbe() {
const catalog = useCharacterCatalog();
return <span>{catalog.characters.map((character) => character.slug).join(",")}</span>;
}
@@ -0,0 +1,47 @@
"use client";
import {
createContext,
type ReactNode,
useContext,
useMemo,
} from "react";
import {
CHARACTERS,
createCharacterCatalog,
type CharacterCatalog,
type CharacterProfile,
} from "@/data/constants/character";
const CharacterCatalogContext = createContext<CharacterCatalog | null>(null);
export interface CharacterCatalogProviderProps {
children: ReactNode;
characters?: readonly CharacterProfile[];
}
export function CharacterCatalogProvider({
children,
characters = CHARACTERS,
}: CharacterCatalogProviderProps) {
const catalog = useMemo(
() => createCharacterCatalog(characters),
[characters],
);
return (
<CharacterCatalogContext.Provider value={catalog}>
{children}
</CharacterCatalogContext.Provider>
);
}
export function useCharacterCatalog(): CharacterCatalog {
const catalog = useContext(CharacterCatalogContext);
if (!catalog) {
throw new Error(
"useCharacterCatalog must be used within CharacterCatalogProvider.",
);
}
return catalog;
}
+19 -16
View File
@@ -3,12 +3,13 @@
/**
* 根级 Client Providers 包装
*
* 这里只保留跨路由的 Auth、User、消息预览和 viewport Provider。Chat、
* Payment、PrivateRoom 状态机由对应路由 layout 按需挂载。
* 这里只保留跨路由的 Character Catalog、Auth、User、消息预览和 viewport
* Provider。Chat、Payment、PrivateRoom 状态机由对应路由 layout 按需挂载。
*/
import type { ReactNode } from "react";
import { CharacterCatalogProvider } from "@/providers/character-catalog-provider";
import { SplashLatestMessageProvider } from "@/providers/splash-latest-message-provider";
import { ViewportCssVarsProvider } from "@/providers/viewport-css-vars-provider";
import { AppNavigationGuard } from "@/router/app-navigation-guard";
@@ -24,19 +25,21 @@ export interface RootProvidersProps {
export function RootProviders({ children }: RootProvidersProps) {
return (
<ViewportCssVarsProvider>
<AuthProvider>
<AuthStatusChecker />
<OAuthSessionSync />
<AppNavigationGuard />
<SplashLatestMessageProvider>
<UserProvider>
<UserAuthSync />
{children}
<div id="toast-portal" />
</UserProvider>
</SplashLatestMessageProvider>
</AuthProvider>
</ViewportCssVarsProvider>
<CharacterCatalogProvider>
<ViewportCssVarsProvider>
<AuthProvider>
<AuthStatusChecker />
<OAuthSessionSync />
<AppNavigationGuard />
<SplashLatestMessageProvider>
<UserProvider>
<UserAuthSync />
{children}
<div id="toast-portal" />
</UserProvider>
</SplashLatestMessageProvider>
</AuthProvider>
</ViewportCssVarsProvider>
</CharacterCatalogProvider>
);
}