feat(frontend): add brand kit management UI and project binding
Add a brand kit domain model, repository, and dedicated page/route for managing brand kits, plus a BrandKitSelector used on the home page and canvas workspace. Home project creation and the workspace can attach a brand kit to a project, with new i18n strings and a side-nav entry. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -2,16 +2,19 @@
|
||||
|
||||
import { ArrowDown, BookOpen, ChevronDown, Circle, Folder, Frame, Globe2, Image as ImageIcon, Layers, Lock, LogOut, Map as MapIcon, Loader2, MessageCircle, MessageSquarePlus, PanelRightClose, PenLine, Search, Trash2, Type, User, Zap } from "lucide-react";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState, type CSSProperties } from "react";
|
||||
import { openHome } from "@/application/route";
|
||||
import { openBrandKit, openHome } from "@/application/route";
|
||||
import { ContextMenu, ContextMenuTrigger } from "@/components/ui/context-menu";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||
import type { BrandKit } from "@/domain/brandKit";
|
||||
import type { AgentContent, AgentMessage, AgentThreadSummary, CanvasNode, CanvasViewport, ExtractedTextLayer, ExtractNodeTextResponse, MockupModel, MockupModelMap, MockupPlacement, MockupSurface, NodeActionRequest, Project, ShareAccess } from "@/domain/design";
|
||||
import { useI18n, type Locale } from "@/i18n/i18n";
|
||||
import { brandKitRepository, brandKitStorageScope } from "@/infrastructure/brandKitRepository";
|
||||
import { designGateway } from "@/infrastructure/designGateway";
|
||||
import { httpStatusOf } from "@/infrastructure/userMessages";
|
||||
import { useAuth } from "@/ui/auth/AuthProvider";
|
||||
import { AccountManagementDialog } from "@/ui/components/AccountManagementDialog";
|
||||
import { ArtboardPreview } from "@/ui/components/ArtboardPreview";
|
||||
import { BrandKitSelector } from "@/ui/components/BrandKitSelector";
|
||||
import { CanvasAnnotationPreview } from "@/ui/components/CanvasAnnotationPreview";
|
||||
import { CanvasAnnotations } from "@/ui/components/CanvasAnnotations";
|
||||
import { CanvasAgentComposer, composerImageSize, defaultComposerImageSettings, type ComposerImageSettings, type ComposerMode } from "@/ui/components/CanvasAgentComposer";
|
||||
@@ -542,10 +545,11 @@ const ownerWorkspaceAccess: ShareAccess = {
|
||||
|
||||
export function CanvasWorkspace({ projectId, shareAccess = ownerWorkspaceAccess }: { projectId: string; shareAccess?: ShareAccess }) {
|
||||
const { t, locale } = useI18n();
|
||||
const { isAuthenticated } = useAuth();
|
||||
const { isAuthenticated, user } = useAuth();
|
||||
const canEdit = shareAccess.capabilities.canEdit;
|
||||
const canViewChat = shareAccess.capabilities.canViewChat;
|
||||
const canManageSharing = shareAccess.capabilities.canManage;
|
||||
const canManageBrandKit = shareAccess.isOwner;
|
||||
const toUserMessageText = useUserMessage();
|
||||
const { showToast } = useGlobalToast();
|
||||
const showGenerationError = useCallback((message: string) => showToast(message, "error"), [showToast]);
|
||||
@@ -585,6 +589,9 @@ export function CanvasWorkspace({ projectId, shareAccess = ownerWorkspaceAccess
|
||||
const [composerMode, setComposerMode] = useState<ComposerMode>("agent");
|
||||
const [useAgentWebSearch, setUseAgentWebSearch] = useState(false);
|
||||
const [selectedCanvasModelId, setSelectedCanvasModelId] = useState("gpt-image-2-auto");
|
||||
const [brandKits, setBrandKits] = useState<BrandKit[]>([]);
|
||||
const [brandKitsLoading, setBrandKitsLoading] = useState(false);
|
||||
const [brandKitUpdating, setBrandKitUpdating] = useState(false);
|
||||
const [composerImageSettings, setComposerImageSettings] = useState<ComposerImageSettings>(defaultComposerImageSettings);
|
||||
const [canvasStageSize, setCanvasStageSize] = useState({ width: 1100, height: 760 });
|
||||
const [mode, setMode] = useState<"select" | "pan">("select");
|
||||
@@ -696,6 +703,32 @@ export function CanvasWorkspace({ projectId, shareAccess = ownerWorkspaceAccess
|
||||
.catch(() => undefined);
|
||||
}, [isAuthenticated]);
|
||||
|
||||
useEffect(() => {
|
||||
let active = true;
|
||||
if (!isAuthenticated || !canManageBrandKit) {
|
||||
setBrandKits([]);
|
||||
setBrandKitsLoading(false);
|
||||
return;
|
||||
}
|
||||
setBrandKitsLoading(true);
|
||||
brandKitRepository
|
||||
.list(brandKitStorageScope(user?.id))
|
||||
.then((kits) => {
|
||||
if (active) setBrandKits(kits);
|
||||
})
|
||||
.catch(() => {
|
||||
if (!active) return;
|
||||
setBrandKits([]);
|
||||
showToast(t("brandKitLoadError"), "error");
|
||||
})
|
||||
.finally(() => {
|
||||
if (active) setBrandKitsLoading(false);
|
||||
});
|
||||
return () => {
|
||||
active = false;
|
||||
};
|
||||
}, [canManageBrandKit, isAuthenticated, showToast, t, user?.id]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!canViewChat) {
|
||||
setAssistantPanelOpen(false);
|
||||
@@ -724,6 +757,25 @@ export function CanvasWorkspace({ projectId, shareAccess = ownerWorkspaceAccess
|
||||
setEditingTextNodeId(null);
|
||||
}, [applyProjectDocument, nodesRef]);
|
||||
|
||||
const selectProjectBrandKit = useCallback(
|
||||
async (brandKitId: string | null) => {
|
||||
const currentProject = projectRef.current;
|
||||
if (!currentProject || !canManageBrandKit) return;
|
||||
setBrandKitUpdating(true);
|
||||
try {
|
||||
const { project: updatedProject } = await designGateway.updateProjectBrandKit(currentProject.id, brandKitId ?? "");
|
||||
projectRef.current = updatedProject;
|
||||
setProject(updatedProject);
|
||||
showToast(t(brandKitId ? "brandKitApplied" : "brandKitCleared"), "success");
|
||||
} catch {
|
||||
showToast(t("brandKitBindingError"), "error");
|
||||
} finally {
|
||||
setBrandKitUpdating(false);
|
||||
}
|
||||
},
|
||||
[canManageBrandKit, projectRef, setProject, showToast, t]
|
||||
);
|
||||
|
||||
const appendThinkingMessage = useCallback((message: AgentMessage) => {
|
||||
if (isImageGeneratorThreadMessage(message, imageGeneratorThreadIdsRef.current)) return;
|
||||
if (message.type === "text_extraction_result" || message.name === "text_extraction") return;
|
||||
@@ -3473,6 +3525,16 @@ export function CanvasWorkspace({ projectId, shareAccess = ownerWorkspaceAccess
|
||||
onBackHome={openHome}
|
||||
onRename={updateProjectTitle}
|
||||
/>
|
||||
{canManageBrandKit && (
|
||||
<BrandKitSelector
|
||||
kits={brandKits}
|
||||
selectedId={project.brandKitId ?? null}
|
||||
loading={brandKitsLoading || brandKitUpdating}
|
||||
disabled={!canEdit || brandKitUpdating || anyGenerating}
|
||||
onSelect={selectProjectBrandKit}
|
||||
onCreate={openBrandKit}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{canViewChat ? (
|
||||
assistantPanelOpen ? (
|
||||
|
||||
Reference in New Issue
Block a user