feat(canvas): 画布主题 - 与工作台外观隔离并提高连线对比

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
ddCat
2026-09-16 22:59:34 +08:00
parent da0531f6eb
commit cb405e9b6a
54 changed files with 207 additions and 134 deletions

View File

@@ -2,7 +2,8 @@ import { useState, type CSSProperties, type ReactNode } from "react";
import { cn } from "@/lib/utils";
import { appearanceLogoURL, useAppearanceStore } from "@/stores/use-appearance-store";
import { useThemeStore, type ThemeName } from "@/stores/use-theme-store";
import type { ThemeName } from "@/stores/use-theme-store";
import { useActiveTheme } from "@/stores/canvas/use-canvas-theme-store";
type BrandLogoProps = {
className?: string;
@@ -13,7 +14,7 @@ type BrandLogoProps = {
export function BrandLogo({ className, fallback, alt = "", theme = "auto" }: BrandLogoProps) {
const appearance = useAppearanceStore((state) => state.appearance);
const currentTheme = useThemeStore((state) => state.theme);
const currentTheme = useActiveTheme();
const source = appearanceLogoURL(appearance, theme === "auto" ? currentTheme : theme);
const [failedSource, setFailedSource] = useState<string | null>(null);
if (!appearance.logoConfigured) return <>{fallback}</>;

View File

@@ -30,7 +30,7 @@ import { useCopyText } from "@/hooks/use-copy-text";
import { resolveImageUrl } from "@/services/image-storage";
import { modelOptionLabel, useEffectiveConfig } from "@/stores/use-config-store";
import { usePluginStore } from "@/stores/use-plugin-store";
import { useThemeStore } from "@/stores/use-theme-store";
import { useActiveTheme } from "@/stores/canvas/use-canvas-theme-store";
import type { CanvasNodeData } from "@/types/canvas";
import { IconButton } from "@/components/ui/base/buttons";
import { Callout } from "@/components/ui/product/callout";
@@ -63,7 +63,7 @@ type AiArtCritiqueView = "overview" | "detail";
type ArtCritiquePromptStatus = "ready" | "pending" | "unavailable";
export function AiArtCritiqueModal({ node, upstreamNodes, open, onClose, onUpdateState, startRequestId, restartRequested, onRunningChange }: AiArtCritiqueModalProps) {
const theme = canvasThemes[useThemeStore((state) => state.theme)];
const theme = canvasThemes[useActiveTheme()];
const effectiveConfig = useEffectiveConfig();
const selectedCritiqueModel = effectiveConfig.textModel.trim();
const configuredCritiqueModelLabel = selectedCritiqueModel ? modelOptionLabel(effectiveConfig, selectedCritiqueModel) : "未配置文本/视觉理解模型";

View File

@@ -7,12 +7,12 @@ import { aceternityMotion } from "@/lib/aceternity-motion";
import { formatTaskKind, generationTaskShowsProgress, generationTaskStageLabel, generationTaskStatusLabel } from "@/lib/generation-task-display";
import { canvasThemes } from "@/lib/canvas-theme";
import type { GenerationTask } from "@/services/api/task-center";
import { useThemeStore } from "@/stores/use-theme-store";
import { useActiveTheme } from "@/stores/canvas/use-canvas-theme-store";
import { useUserStore } from "@/stores/use-user-store";
// 顶栏是绝对定位浮层,面板必须按调用方传入的 topInset 避让;专注模式隐藏顶栏时传小间距。
export function CanvasActiveTaskPanel({ tasks, align = "right", topInset = "var(--canvas-topbar-offset)", onCancelTask }: { tasks: GenerationTask[]; align?: "left" | "right"; topInset?: string; onCancelTask?: (task: GenerationTask) => void }) {
const theme = canvasThemes[useThemeStore((state) => state.theme)];
const theme = canvasThemes[useActiveTheme()];
const creditsEnabled = useUserStore((state) => state.features.creditsEnabled);
const reducedMotion = useReducedMotion();
const [now, setNow] = useState(() => Date.now());

View File

@@ -10,7 +10,7 @@ import { canvasDockStyle } from "@/lib/canvas/canvas-aceternity-style";
import { canvasThemes } from "@/lib/canvas-theme";
import { resourceStorageLabel, resourceStorageLocation, resourceStorageTitle } from "@/lib/canvas/resource-storage-status";
import { cn } from "@/lib/utils";
import { useThemeStore } from "@/stores/use-theme-store";
import { useActiveTheme } from "@/stores/canvas/use-canvas-theme-store";
import type { ImageAsset } from "@/stores/use-asset-store";
import { CanvasNodeType, type CanvasNodeData } from "@/types/canvas";
@@ -51,7 +51,7 @@ type CanvasAssetTrayProps = {
};
export function CanvasAssetTray({ assetImages, canvasImages, showLibrary = true, activeNodeId, onInsertAssetImage, onFocusCanvasImage }: CanvasAssetTrayProps) {
const theme = canvasThemes[useThemeStore((state) => state.theme)];
const theme = canvasThemes[useActiveTheme()];
const reducedMotion = useReducedMotion();
const { bringToFront, zIndex } = useCanvasOverlayLayer("asset-tray", "var(--z-panel)");
const rootRef = useRef<HTMLDivElement>(null);
@@ -265,7 +265,7 @@ function TrayTabButton({ active, label, theme, onClick }: { active: boolean; lab
}
function AssetTrayRow({ title, imageUrl, storageKey, icon, active = false, draggable = false, motionEnabled, onClick, onDragStart }: { title: string; imageUrl: string; storageKey?: string; icon: ReactNode; active?: boolean; draggable?: boolean; motionEnabled: boolean; onClick: () => void; onDragStart?: (event: DragEvent<HTMLElement>) => void }) {
const theme = canvasThemes[useThemeStore((state) => state.theme)];
const theme = canvasThemes[useActiveTheme()];
const location = resourceStorageLocation(storageKey);
return (
<motion.button

View File

@@ -6,7 +6,7 @@ import { Button } from "antd";
import { AudioSettingsPanel } from "@/components/audio-settings-panel";
import { audioFormatLabel, audioSpeedLabel, audioVoiceLabel } from "@/lib/audio-generation";
import { canvasThemes } from "@/lib/canvas-theme";
import { useThemeStore } from "@/stores/use-theme-store";
import { useActiveTheme } from "@/stores/canvas/use-canvas-theme-store";
import type { AiConfig } from "@/stores/use-config-store";
export type CanvasAudioSettingKey = "audioVoice" | "audioFormat" | "audioSpeed" | "audioInstructions";
@@ -19,7 +19,7 @@ type CanvasAudioSettingsPopoverProps = {
};
export function CanvasAudioSettingsPopover({ config, onConfigChange, buttonClassName, placement = "topLeft" }: CanvasAudioSettingsPopoverProps) {
const theme = canvasThemes[useThemeStore((state) => state.theme)];
const theme = canvasThemes[useActiveTheme()];
const buttonRef = useRef<HTMLSpanElement>(null);
const panelRef = useRef<HTMLDivElement>(null);
const [open, setOpen] = useState(false);

View File

@@ -5,7 +5,7 @@ import { FileText, Image as ImageIcon, Music2, Pencil, Sparkles, Video, X } from
import { canvasThemes } from "@/lib/canvas-theme";
import { isCanvasWorkflowProvider } from "@/lib/canvas/canvas-workflow";
import { useThemeStore } from "@/stores/use-theme-store";
import { useActiveTheme } from "@/stores/canvas/use-canvas-theme-store";
import type { CanvasResourceReference } from "@/lib/canvas/canvas-resource-references";
import { generationInputMentionLabel, normalizeGenerationNodeMentionTokens, type NodeGenerationInput } from "./canvas-node-generation";
import { CanvasVideoPromptTools } from "./canvas-video-prompt-tools";
@@ -45,7 +45,7 @@ type ComposerCandidate =
export const CONFIG_REFERENCE_PATTERN = /@\[node:([^\]]+)\]|@(图片|视频|音频|文本|角色|绘图)(\d+)/g;
export function CanvasConfigComposer({ value, inputs, skillReferences = [], generationMode, metadata, onChange, onMetadataChange, onClose, workspaceMode = "professional" }: CanvasConfigComposerProps) {
const theme = canvasThemes[useThemeStore((state) => state.theme)];
const theme = canvasThemes[useActiveTheme()];
const editorRef = useRef<HTMLDivElement>(null);
const composingRef = useRef(false);
const [mention, setMention] = useState<MentionState | null>(null);

View File

@@ -12,7 +12,7 @@ import { defaultModelCapabilityConfig, modelCapabilityConfigFor, normalizeImageV
import { defaultImageParamsForModel, modelCompatibilityError, modelRequestOptions, resolveCompatibleModel, resolveModelGenerationDefaults, type ModelRequirements } from "@/lib/model-selection";
import { resolveCanvasWorkflowProvider } from "@/lib/canvas/canvas-workflow";
import type { CanvasAudioSettingKey } from "./canvas-audio-settings-popover";
import { useThemeStore } from "@/stores/use-theme-store";
import { useActiveTheme } from "@/stores/canvas/use-canvas-theme-store";
import { workflowProviderPluginEnabled } from "@/lib/plugins/builtin/workflows";
import { usePluginStore } from "@/stores/use-plugin-store";
import type { CanvasGenerationMode, CanvasNodeData, CanvasNodeMetadata, CanvasVideoEditOperation, CanvasWorkspaceMode } from "@/types/canvas";
@@ -64,7 +64,7 @@ function runningHubWorkflowEntryKey(workflow: RunningHubWorkflow): string {
export function CanvasConfigNodePanel({ node, isRunning, inputSummary, onConfigChange, onGenerate, onComposerToggle, workspaceMode = "professional" }: CanvasConfigNodePanelProps) {
const globalConfig = useEffectiveConfig();
const runtimeStatuses = usePluginStore((state) => state.runtimeStatuses);
const theme = canvasThemes[useThemeStore((state) => state.theme)];
const theme = canvasThemes[useActiveTheme()];
const mode = node.metadata?.generationMode === "video" || node.metadata?.generationMode === "audio" ? node.metadata.generationMode : "image";
const simpleMode = workspaceMode === "simple";
const resolvedProvider = resolveCanvasWorkflowProvider(node.metadata);

View File

@@ -2,7 +2,7 @@ import React, { useState } from "react";
import type { MouseEvent as ReactMouseEvent } from "react";
import { canvasThemes } from "@/lib/canvas-theme";
import { useThemeStore } from "@/stores/use-theme-store";
import { useActiveTheme } from "@/stores/canvas/use-canvas-theme-store";
import { STORYBOARD_HEADER_HEIGHT, STORYBOARD_ROW_HEIGHT, storyboardTableHeight } from "@/lib/canvas/canvas-storyboard-layout";
import { batchReferenceHandleY } from "@/lib/canvas/canvas-batch-table";
import type { CanvasConnection, CanvasNodeData, ConnectionHandle, Position } from "@/types/canvas";
@@ -30,7 +30,7 @@ export const ConnectionPath = React.memo(function ConnectionPath({
onSelect: () => void;
onContextMenu?: (event: ReactMouseEvent<SVGPathElement>) => void;
}) {
const theme = canvasThemes[useThemeStore((state) => state.theme)];
const theme = canvasThemes[useActiveTheme()];
const [hovered, setHovered] = useState(false);
const { pathD, startX, startY, endX, endY } = canvasConnectionPath(connection, from, to, fromScrollTop, toScrollTop);
const emphasized = active || hovered;
@@ -100,9 +100,9 @@ export const ConnectionPath = React.memo(function ConnectionPath({
{showVisual ? <path
d={pathD}
stroke={emphasized ? theme.accent.primary : theme.node.muted}
strokeWidth={emphasized ? 2.2 : 1.5}
strokeWidth={emphasized ? 2.8 : 2}
vectorEffect="non-scaling-stroke"
strokeOpacity={emphasized ? 0.9 : 0.72}
strokeOpacity={emphasized ? 0.95 : 0.8}
fill="none"
strokeLinecap="round"
strokeLinejoin="round"

View File

@@ -10,7 +10,7 @@ import { canvasThemes } from "@/lib/canvas-theme";
import { canvasNodeAssetCategory } from "@/lib/canvas/canvas-node-asset";
import { isCanvasFolderNode } from "@/lib/canvas/canvas-frame";
import { resolveAddNodeMenuCommands, type AddNodeMenuContext } from "@/lib/canvas/tool-registry";
import { useThemeStore } from "@/stores/use-theme-store";
import { useActiveTheme } from "@/stores/canvas/use-canvas-theme-store";
import { usePluginStore } from "@/stores/use-plugin-store";
import { CanvasNodeType, type CanvasNodeData, type CanvasNodeTypeId, type CanvasWorkspaceMode, type ContextMenuState, type Position } from "@/types/canvas";
@@ -95,7 +95,7 @@ export function CanvasNodeContextMenu({
onCopySelection,
onDeleteSelection,
}: CanvasNodeContextMenuProps) {
const theme = canvasThemes[useThemeStore((state) => state.theme)];
const theme = canvasThemes[useActiveTheme()];
const [addOpen, setAddOpen] = useState(false);
const [categoryOpen, setCategoryOpen] = useState(false);
@@ -279,7 +279,7 @@ export function CanvasNodeContextMenu({
}
function AddNodeContextMenu({ parentPosition, workspaceMode, isProjectLinked, onAddNode, onAddFolder, onChooseStyle, onOpenDirector, onUpload, onOpenAssets, onOpenProjectCharacters }: { parentPosition: { left: number; top: number }; workspaceMode: CanvasWorkspaceMode; isProjectLinked: boolean; onAddNode: (type: CanvasNodeTypeId) => void; onAddFolder: () => void; onChooseStyle: () => void; onOpenDirector: () => void; onUpload: () => void; onOpenAssets: () => void; onOpenProjectCharacters: () => void }) {
const theme = canvasThemes[useThemeStore((state) => state.theme)];
const theme = canvasThemes[useActiveTheme()];
const installations = usePluginStore((state) => state.installations);
const pluginStates = usePluginStore((state) => state.pluginStates);
const left = getSubmenuLeft(parentPosition.left);
@@ -332,7 +332,7 @@ function AddNodeContextMenu({ parentPosition, workspaceMode, isProjectLinked, on
}
function MenuHeader({ title, description, onBack }: { title: string; description?: string; onBack?: () => void }) {
const theme = canvasThemes[useThemeStore((state) => state.theme)];
const theme = canvasThemes[useActiveTheme()];
return (
<div className="mb-0.5 flex items-start gap-1 px-1.5 py-1.5">
{onBack ? <button type="button" onClick={onBack} className="mt-0.5 grid size-6 shrink-0 place-items-center rounded-md outline-none hover:bg-black/5 focus-visible:ring-2 dark:hover:bg-white/8" aria-label="返回媒体操作"><ArrowLeft className="size-3.5" /></button> : null}
@@ -346,7 +346,7 @@ function MenuSection({ label }: { label: string }) {
}
function MenuButton({ icon, label, detail, shortcut, badge, chevron = false, active = false, disabled = false, danger = false, onClick }: { icon: ReactNode; label: string; detail?: string; shortcut?: string; badge?: string; chevron?: boolean; active?: boolean; disabled?: boolean; danger?: boolean; onClick?: () => void }) {
const theme = canvasThemes[useThemeStore((state) => state.theme)];
const theme = canvasThemes[useActiveTheme()];
const color = danger ? theme.accent.danger : theme.node.text;
return (
<button
@@ -365,7 +365,7 @@ function MenuButton({ icon, label, detail, shortcut, badge, chevron = false, act
}
function MenuDivider() {
const theme = canvasThemes[useThemeStore((state) => state.theme)];
const theme = canvasThemes[useActiveTheme()];
return <div className="mx-1.5 my-1 h-px" style={{ background: `linear-gradient(90deg, transparent, ${theme.toolbar.border}, transparent)` }} />;
}

View File

@@ -4,7 +4,7 @@ import type { CSSProperties, ReactNode } from "react";
import { aceternityMotion } from "@/lib/aceternity-motion";
import { canvasThemes } from "@/lib/canvas-theme";
import { cn } from "@/lib/utils";
import { useThemeStore } from "@/stores/use-theme-store";
import { useActiveTheme } from "@/stores/canvas/use-canvas-theme-store";
export type CanvasCreateCommand = {
id: string;
@@ -16,7 +16,7 @@ export type CanvasCreateCommand = {
};
export function CanvasCreateMenu({ commands }: { commands: CanvasCreateCommand[] }) {
const theme = canvasThemes[useThemeStore((state) => state.theme)];
const theme = canvasThemes[useActiveTheme()];
const projectCommands = commands.filter((command) => command.section === "project");
const nodeCommands = commands.filter((command) => command.section === "node");
const workflowCommands = commands.filter((command) => command.section === "workflow");
@@ -59,7 +59,7 @@ export function CanvasCreateMenu({ commands }: { commands: CanvasCreateCommand[]
}
function CanvasCreateCommandGrid({ commands, variant }: { commands: CanvasCreateCommand[]; variant: "node" | "compact" | "workflow" }) {
const theme = canvasThemes[useThemeStore((state) => state.theme)];
const theme = canvasThemes[useActiveTheme()];
const reducedMotion = useReducedMotion();
return (

View File

@@ -6,7 +6,7 @@ import { Check, Maximize2, X } from "lucide-react";
import type { CanvasDrawingEditorHandle } from "@/components/canvas/canvas-drawing-editor-types";
import { drawingEngineForNode, drawingEngineLabel, isDrawingEngineAvailable } from "@/lib/canvas/canvas-drawing-engine";
import { loadCanvasDrawing, saveCanvasDrawing, type CanvasDrawingSnapshot } from "@/lib/canvas/canvas-drawing-storage";
import { useThemeStore } from "@/stores/use-theme-store";
import { useActiveTheme } from "@/stores/canvas/use-canvas-theme-store";
import { useUserStore } from "@/stores/use-user-store";
import type { CanvasNodeData } from "@/types/canvas";
@@ -23,7 +23,7 @@ type CanvasDrawingEditorModalProps = {
export function CanvasDrawingEditorModal({ open, projectId, node, onClose, onSaved }: CanvasDrawingEditorModalProps) {
const { message } = App.useApp();
const colorScheme = useThemeStore((state) => state.theme);
const colorScheme = useActiveTheme();
const tldrawLicenseKey = useUserStore((state) => state.drawingEngine.tldrawLicenseKey);
const engine = drawingEngineForNode(node);
const currentRef = useRef<CanvasDrawingSnapshot | null>(null);

View File

@@ -11,7 +11,7 @@ import { canvasThemes } from "@/lib/canvas-theme";
import { buildEmotionImageArtifacts, buildEmotionPrompt, neutralEmotionPreset, type CanvasEmotionPreset, type CanvasFaceBox } from "@/lib/canvas/canvas-emotion";
import { detectCanvasFaces } from "@/lib/canvas/canvas-face-detection";
import { subscribeCanvasViewportPreview } from "@/lib/canvas/canvas-live-viewport";
import { useThemeStore } from "@/stores/use-theme-store";
import { useActiveTheme } from "@/stores/canvas/use-canvas-theme-store";
import type { CanvasNodeData, Position, ViewportTransform } from "@/types/canvas";
type WorkspaceStatus = "detecting" | "selecting" | "manual" | "editing" | "generating" | "error";
@@ -193,7 +193,7 @@ function FaceSelectionOverlay({
onManualComplete: (box: CanvasFaceBox) => void;
onFaceSelect: (box: CanvasFaceBox) => void;
}) {
const theme = canvasThemes[useThemeStore((state) => state.theme)];
const theme = canvasThemes[useActiveTheme()];
const overlayRef = useRef<HTMLDivElement>(null);
const dragStartRef = useRef<{ x: number; y: number } | null>(null);
useScreenAnchor(overlayRef, node, viewport, containerRef, (next) => imageScreenRect(node, next, imageWidth, imageHeight));
@@ -352,7 +352,7 @@ function SelectionToolbar({
onManualSelect: () => void;
onClose: () => void;
}) {
const theme = canvasThemes[useThemeStore((state) => state.theme)];
const theme = canvasThemes[useActiveTheme()];
const reducedMotion = useReducedMotion();
const toolbarRef = useRef<HTMLDivElement>(null);
useScreenAnchor(toolbarRef, node, viewport, containerRef, (next, container) => toolbarScreenRect(node, next, container, toolbarRef.current));

View File

@@ -5,7 +5,7 @@ import { PanelBottom, X, ZoomIn, ZoomOut } from "lucide-react";
import { aceternityMotion } from "@/lib/aceternity-motion";
import { canvasThemes } from "@/lib/canvas-theme";
import { useThemeStore } from "@/stores/use-theme-store";
import { useActiveTheme } from "@/stores/canvas/use-canvas-theme-store";
type CanvasFocusModeBarProps = {
dockRevealed: boolean;
@@ -18,7 +18,7 @@ type CanvasFocusModeBarProps = {
};
export function CanvasFocusModeBar({ dockRevealed, zoomPercent, onToggleDock, onExit, onZoomIn, onZoomOut, onFit }: CanvasFocusModeBarProps) {
const theme = canvasThemes[useThemeStore((state) => state.theme)];
const theme = canvasThemes[useActiveTheme()];
const reducedMotion = useReducedMotion();
return (

View File

@@ -7,7 +7,7 @@ import { CanvasFolderPreview } from "@/components/canvas/canvas-folder-preview";
import { FRAME_HEADER_HEIGHT, FRAME_PADDING, isCanvasFolderNode } from "@/lib/canvas/canvas-frame";
import { canvasNodeVideoPreviewUrl } from "@/lib/canvas/canvas-media-preview";
import { canvasThemes, type CanvasTheme } from "@/lib/canvas-theme";
import { useThemeStore } from "@/stores/use-theme-store";
import { useActiveTheme } from "@/stores/canvas/use-canvas-theme-store";
import { CanvasNodeType, type CanvasFolderStyle, type CanvasFolderTheme, type CanvasNodeData, type Position } from "@/types/canvas";
type ResizeCorner = "top-left" | "top-right" | "bottom-left" | "bottom-right";
@@ -47,7 +47,7 @@ export const CanvasFrameNode = React.memo(function CanvasFrameNode({
onHoverStart?: (nodeId: string) => void;
onHoverEnd?: (nodeId: string) => void;
}) {
const theme = canvasThemes[useThemeStore((state) => state.theme)];
const theme = canvasThemes[useActiveTheme()];
const collapsed = Boolean(data.metadata?.frame?.collapsed);
const folder = isCanvasFolderNode(data);
const [editing, setEditing] = useState(false);

View File

@@ -6,7 +6,7 @@ import { Button } from "antd";
import { ImageSettingsPanel, imageQualityLabel, imageSizeLabel } from "@/components/image-settings-panel";
import { canvasThemes } from "@/lib/canvas-theme";
import { modelCapabilityConfigFor, normalizeImageValue } from "@/lib/model-capabilities";
import { useThemeStore } from "@/stores/use-theme-store";
import { useActiveTheme } from "@/stores/canvas/use-canvas-theme-store";
import type { AiConfig } from "@/stores/use-config-store";
type CanvasImageSettingsPopoverProps = {
@@ -22,7 +22,7 @@ type CanvasImageSettingsPopoverProps = {
};
export function CanvasImageSettingsPopover({ config, onConfigChange, onOpenChange, buttonClassName, placement = "topLeft", showCount = true }: CanvasImageSettingsPopoverProps) {
const theme = canvasThemes[useThemeStore((state) => state.theme)];
const theme = canvasThemes[useActiveTheme()];
const buttonRef = useRef<HTMLSpanElement>(null);
const panelRef = useRef<HTMLDivElement>(null);
const [open, setOpen] = useState(false);

View File

@@ -282,10 +282,10 @@ function syncConnectionPath(entry: ConnectionSceneEntry, props: CanvasLeaferGrap
entry.path.set({
path: canvasConnectionPath(entry.connection, from, to, props.scriptScrollTopById[entry.from.id] || 0, props.scriptScrollTopById[entry.to.id] || 0).pathD,
stroke: emphasized ? props.theme.accent.primary : props.theme.node.muted,
strokeWidth: emphasized ? 1.6 : 1,
strokeWidth: emphasized ? 2.8 : 2,
strokeScaleFixed: true,
strokeCap: "round",
opacity: emphasized ? 0.52 : 0.24,
opacity: emphasized ? 0.95 : 0.8,
hittable: false,
});
}

View File

@@ -6,7 +6,7 @@ import { isFrameNode, isNodeHiddenByCollapsedFrame } from "@/lib/canvas/canvas-f
import { buildLibTVImagePreviewUrl } from "@/lib/canvas/libtv-import";
import { getNodeLabel } from "@/lib/canvas/node-registry/node-registry";
import { subscribeCanvasViewportPreview } from "@/lib/canvas/canvas-live-viewport";
import { useThemeStore } from "@/stores/use-theme-store";
import { useActiveTheme } from "@/stores/canvas/use-canvas-theme-store";
import { CanvasNodeType, type CanvasNodeData, type ViewportTransform } from "@/types/canvas";
const MINIMAP_WIDTH = 240;
@@ -14,7 +14,7 @@ const MINIMAP_HEIGHT = 160;
const MINIMAP_IMAGE_PREVIEW_LIMIT = 24;
export function Minimap({ nodes, viewport, viewportSize, canvasContainerRef, onViewportPreviewChange, onViewportChange }: { nodes: CanvasNodeData[]; viewport: ViewportTransform; viewportSize: { width: number; height: number }; canvasContainerRef?: RefObject<HTMLDivElement | null>; onViewportPreviewChange?: (viewport: ViewportTransform) => void; onViewportChange: (viewport: ViewportTransform) => void }) {
const theme = canvasThemes[useThemeStore((state) => state.theme)];
const theme = canvasThemes[useActiveTheme()];
const containerRef = useRef<HTMLDivElement>(null);
const viewportRectRef = useRef<HTMLDivElement>(null);
const liveViewportRef = useRef(viewport);

View File

@@ -8,7 +8,7 @@ import { CanvasAngleScene } from "@/components/canvas/canvas-angle-scene";
import { SpotlightSurface } from "@/components/ui/aceternity/spotlight-surface";
import { aceternityMotion } from "@/lib/aceternity-motion";
import { canvasThemes } from "@/lib/canvas-theme";
import { useThemeStore } from "@/stores/use-theme-store";
import { useActiveTheme } from "@/stores/canvas/use-canvas-theme-store";
export type CanvasImageAngleParams = {
horizontalAngle: number;
@@ -36,7 +36,7 @@ function distanceLabel(value: number) {
}
export function CanvasNodeAnglePanel({ dataUrl, onClose, onConfirm }: { dataUrl: string; onClose: () => void; onConfirm: (params: CanvasImageAngleParams) => void }) {
const theme = canvasThemes[useThemeStore((state) => state.theme)];
const theme = canvasThemes[useActiveTheme()];
const reducedMotion = useReducedMotion();
const [params, setParams] = useState(defaultParams);
const [mode, setMode] = useState<"camera" | "skybox">("camera");

View File

@@ -8,7 +8,7 @@ import { Tooltip } from "@/components/ui/base/tooltip";
import { aceternityMotion } from "@/lib/aceternity-motion";
import { canvasThemes } from "@/lib/canvas-theme";
import { useCopyText } from "@/hooks/use-copy-text";
import { useThemeStore } from "@/stores/use-theme-store";
import { useActiveTheme } from "@/stores/canvas/use-canvas-theme-store";
import {
APERTURES,
APERTURE_META,
@@ -139,7 +139,7 @@ interface HoverTipProps {
}
const HoverTip = memo(({ title, description, useCase, children }: HoverTipProps) => {
const theme = canvasThemes[useThemeStore((state) => state.theme)];
const theme = canvasThemes[useActiveTheme()];
const [coords, setCoords] = useState<{ left: number; top: number; placement: "top" | "bottom" } | null>(null);
const anchorRef = useRef<HTMLDivElement | null>(null);
const tipRef = useRef<HTMLDivElement | null>(null);
@@ -282,7 +282,7 @@ export function CanvasNodeCameraPanel({
onClose: () => void;
onConfirm: (options: CameraControlOptions, prompt: string) => void;
}) {
const theme = canvasThemes[useThemeStore((state) => state.theme)];
const theme = canvasThemes[useActiveTheme()];
const reducedMotion = useReducedMotion();
const copyText = useCopyText();

View File

@@ -18,7 +18,7 @@ import {
type CanvasEmotionEditRegion,
type CanvasFaceBox,
} from "@/lib/canvas/canvas-emotion";
import { useThemeStore } from "@/stores/use-theme-store";
import { useActiveTheme } from "@/stores/canvas/use-canvas-theme-store";
export type CanvasImageEmotionPayload = CanvasEmotionParams & {
label: string;
@@ -54,7 +54,7 @@ type CanvasNodeEmotionPanelProps = {
};
export function CanvasNodeEmotionPanel({ dataUrl, imageWidth, imageHeight, characters, activeCharacterId, preset, generating, error, onSelectCharacter, onManualSelect, onPresetChange, onClose, onConfirm }: CanvasNodeEmotionPanelProps) {
const theme = canvasThemes[useThemeStore((state) => state.theme)];
const theme = canvasThemes[useActiveTheme()];
const reducedMotion = useReducedMotion();
return (
<SpotlightSurface
@@ -129,7 +129,7 @@ function FaceThumbnail({ dataUrl, imageWidth, imageHeight, box }: { dataUrl: str
}
function EmotionPad({ preset, onChange }: { preset: CanvasEmotionPreset; onChange: (preset: CanvasEmotionPreset) => void }) {
const theme = canvasThemes[useThemeStore((state) => state.theme)];
const theme = canvasThemes[useActiveTheme()];
const reducedMotion = useReducedMotion();
const [pointer, setPointer] = useState<{ x: number; y: number } | null>(null);
const [dragging, setDragging] = useState(false);
@@ -190,7 +190,7 @@ function EmotionPad({ preset, onChange }: { preset: CanvasEmotionPreset; onChang
}
function EmotionHeadPreview({ preset }: { preset: CanvasEmotionPreset }) {
const theme = canvasThemes[useThemeStore((state) => state.theme)];
const theme = canvasThemes[useActiveTheme()];
return (
<div className="relative overflow-hidden rounded-[var(--r-lg)] border" style={{ background: "#26272a", borderColor: theme.toolbar.border }}>
<Canvas frameloop="demand" dpr={[1, 1.5]} camera={{ fov: 38, near: 0.1, far: 20, position: [0, 0, 4.15] }} gl={{ antialias: true, alpha: false, powerPreference: "high-performance" }}>

View File

@@ -9,7 +9,7 @@ import { Tooltip } from "@/components/ui/base/tooltip";
import { aceternityMotion } from "@/lib/aceternity-motion";
import { canvasThemes } from "@/lib/canvas-theme";
import { useCopyText } from "@/hooks/use-copy-text";
import { useThemeStore } from "@/stores/use-theme-store";
import { useActiveTheme } from "@/stores/canvas/use-canvas-theme-store";
export type CanvasImageLightingOptions = {
azimuth: number;
@@ -141,7 +141,7 @@ function buildLightingPrompt(input: {
}
export function CanvasNodeLightingPanel({ dataUrl, onClose, onConfirm }: { dataUrl: string; onClose: () => void; onConfirm: (options: CanvasImageLightingOptions, prompt: string) => void }) {
const theme = canvasThemes[useThemeStore((state) => state.theme)];
const theme = canvasThemes[useActiveTheme()];
const reducedMotion = useReducedMotion();
const copyText = useCopyText();
const [options, setOptions] = useState(defaultLightingOptions);

View File

@@ -8,7 +8,7 @@ import { ModelPicker } from "@/components/model-picker";
import { defaultImageParamsForModel } from "@/lib/model-selection";
import type { AiConfig } from "@/stores/use-config-store";
import { canvasThemes } from "@/lib/canvas-theme";
import { useThemeStore } from "@/stores/use-theme-store";
import { useActiveTheme } from "@/stores/canvas/use-canvas-theme-store";
export type CanvasImageMaskEditPayload = {
prompt: string;
@@ -33,7 +33,7 @@ export function CanvasNodeMaskEditDialog({ dataUrl, open, config, onClose, onCon
const [error, setError] = useState("");
const [generationConfig, setGenerationConfig] = useState<AiConfig>(() => config);
const [advancedOpen, setAdvancedOpen] = useState(false);
const theme = canvasThemes[useThemeStore((state) => state.theme)];
const theme = canvasThemes[useActiveTheme()];
useEffect(() => {
if (!open) return;

View File

@@ -12,7 +12,7 @@ import { modelQuoteRequest } from "@/lib/model-pricing";
import { normalizeVideoDuration, normalizeVideoResolution } from "@/lib/video-generation-options";
import { modelRequestOptions, resolveCompatibleModel, resolveModelGenerationDefaults, defaultImageParamsForModel, type ModelRequirements } from "@/lib/model-selection";
import { navigateToSettings } from "@/lib/settings-navigation";
import { useThemeStore } from "@/stores/use-theme-store";
import { useActiveTheme } from "@/stores/canvas/use-canvas-theme-store";
import { useUserStore } from "@/stores/use-user-store";
import { CanvasCameraControlPopover } from "./canvas-camera-control-popover";
import { CanvasImageSettingsPopover } from "./canvas-image-settings-popover";
@@ -66,7 +66,7 @@ const PROMPT_EDITOR_MAX_LINES = 8;
export function CanvasNodePromptPanel({ projectId, node, isRunning, onPromptChange, onConfigChange, onGenerate, mentionReferences = [], onAddReference, onRemoveReference, onReorderReferences, onReplaceReference, onReplaceReferenceFiles, onClose, onNodeMouseDown, onImageSettingsOpenChange, workspaceMode = "professional" }: CanvasNodePromptPanelProps) {
const globalConfig = useEffectiveConfig();
const themeName = useThemeStore((state) => state.theme);
const themeName = useActiveTheme();
const theme = canvasThemes[themeName];
const creditsEnabled = useUserStore((state) => state.features.creditsEnabled);
const promptOptimizerInstallation = usePluginStore((state) => state.installations.find((item) => item.manifest.id === PROMPT_OPTIMIZER_PLUGIN_ID));

View File

@@ -13,7 +13,7 @@ import type { ImageSplitParams } from "@/lib/canvas/canvas-image-data";
import { formatBytes, getDataUrlByteSize } from "@/lib/image-utils";
import { generationErrorMessage } from "@/lib/generation-error";
import { useCopyText } from "@/hooks/use-copy-text";
import { useThemeStore } from "@/stores/use-theme-store";
import { useActiveTheme } from "@/stores/canvas/use-canvas-theme-store";
import { CanvasNodeType, type CanvasNodeData, type CanvasNodeMetadata, type CanvasWorkspaceMode, type ViewportTransform } from "@/types/canvas";
import { buildImageToolbarTools } from "./canvas-image-toolbar-tools";
import { CanvasGridSplitPicker } from "./canvas-grid-split-picker";
@@ -127,7 +127,7 @@ export function CanvasNodeToolbar({
const toolbarRef = useRef<HTMLDivElement>(null);
const { message } = App.useApp();
const copyText = useCopyText();
const themeName = useThemeStore((state) => state.theme);
const themeName = useActiveTheme();
const theme = canvasThemes[themeName];
const simpleMode = workspaceMode === "simple";
@@ -486,7 +486,7 @@ function NodeDockMenuButton({ menuId, label, icon, tools, openMenuId, onOpenChan
}
export function CanvasNodeInfoModal({ node, open, onClose, onMetadataChange, readOnly = false, onUnauthorized }: { node: CanvasNodeData | null; open: boolean; onClose: () => void; onMetadataChange?: (nodeId: string, metadata: Partial<CanvasNodeMetadata>) => void; readOnly?: boolean; onUnauthorized?: () => void }) {
const theme = canvasThemes[useThemeStore((state) => state.theme)];
const theme = canvasThemes[useActiveTheme()];
const [assetTags, setAssetTags] = useState<string[]>([]);
const [assetTagInput, setAssetTagInput] = useState("");
const [assetCategory, setAssetCategory] = useState<CanvasAssetCategory>("other");

View File

@@ -8,7 +8,7 @@ import { canvasThemes } from "@/lib/canvas-theme";
import { canvasConnectionTilt } from "@/lib/canvas/canvas-connection-tilt";
import { storyboardMinNodeHeight } from "@/lib/canvas/canvas-storyboard-layout";
import { resourceStorageLabel, resourceStorageLocation, resourceStorageTitle } from "@/lib/canvas/resource-storage-status";
import { useThemeStore } from "@/stores/use-theme-store";
import { useActiveTheme } from "@/stores/canvas/use-canvas-theme-store";
import { CanvasNodeType, type CanvasNodeData, type CanvasNodeTypeId, type Position } from "@/types/canvas";
import type { CanvasResourceReference } from "@/lib/canvas/canvas-resource-references";
import { ART_CRITIQUE_NODE_TYPE } from "@/lib/art-critique/contracts";
@@ -112,7 +112,7 @@ export const CanvasNode = React.memo(function CanvasNode({
onMediaPlayRequest,
onContextMenu,
}: CanvasNodeProps) {
const theme = canvasThemes[useThemeStore((state) => state.theme)];
const theme = canvasThemes[useActiveTheme()];
const [hovered, setHovered] = useState(false);
const [isEditingContent, setIsEditingContent] = useState(false);
const [isEditingTitle, setIsEditingTitle] = useState(false);

View File

@@ -8,7 +8,7 @@ import {
type PortraitTextureSettingKey,
type PortraitTextureSettings,
} from "@/lib/canvas/canvas-portrait-texture";
import { useThemeStore } from "@/stores/use-theme-store";
import { useActiveTheme } from "@/stores/canvas/use-canvas-theme-store";
type CanvasPortraitTexturePopoverProps = {
value: unknown;
@@ -17,7 +17,7 @@ type CanvasPortraitTexturePopoverProps = {
};
export function CanvasPortraitTexturePopover({ value, placement = "topLeft", onChange }: CanvasPortraitTexturePopoverProps) {
const theme = canvasThemes[useThemeStore((state) => state.theme)];
const theme = canvasThemes[useActiveTheme()];
const settings = normalizePortraitTextureSettings(value);
const updateSetting = (key: PortraitTextureSettingKey, nextValue: string) => {

View File

@@ -4,7 +4,7 @@ import { LayoutTemplate, Search, WandSparkles } from "lucide-react";
import { canvasThemes } from "@/lib/canvas-theme";
import { CANVAS_BUILTIN_PRESETS, type CanvasPromptPreset } from "@/lib/prompts";
import { useThemeStore } from "@/stores/use-theme-store";
import { useActiveTheme } from "@/stores/canvas/use-canvas-theme-store";
import type { CanvasGenerationMode } from "@/types/canvas";
import type { CanvasResourceReference } from "@/lib/canvas/canvas-resource-references";
@@ -29,7 +29,7 @@ export function CanvasPresetPicker({
dense?: boolean;
appearance?: "default" | "quiet";
}) {
const theme = canvasThemes[useThemeStore((state) => state.theme)];
const theme = canvasThemes[useActiveTheme()];
const [internalOpen, setInternalOpen] = useState(false);
const [query, setQuery] = useState("");
const actualOpen = open ?? internalOpen;

View File

@@ -5,7 +5,7 @@ import { ArrowLeft, ChevronRight, FileText, Folder, Image as ImageIcon, Music2,
import { canvasThemes } from "@/lib/canvas-theme";
import { ASSET_CATEGORY_LABELS } from "@/lib/asset-category";
import { useThemeStore } from "@/stores/use-theme-store";
import { useActiveTheme } from "@/stores/canvas/use-canvas-theme-store";
import { buildAssetMentionReferences, canvasResourceMentionToken, findCanvasResourceAutoLinkMatch, type CanvasResourceAutoLinkMatch, type CanvasResourceReference } from "@/lib/canvas/canvas-resource-references";
import { useAssetStore, type AssetCategory } from "@/stores/use-asset-store";
import { CanvasNodeType } from "@/types/canvas";
@@ -54,7 +54,7 @@ export const CanvasResourceMentionTextarea = forwardRef<HTMLTextAreaElement, Pro
{ value, references, onSelectReference, onChange, onSubmit, onKeyDown, className, containerClassName, style, highlightLabels = true, mentionMenuWidth = 320, sendOnEnter = true, onContentSizeChange, includeAssetLibrary = false, activeDropReferenceId, onReferenceFilesDrop, autoLinkEnabled = false, ...props },
forwardedRef,
) {
const rawTheme = useThemeStore((state) => state.theme);
const rawTheme = useActiveTheme();
const assets = useAssetStore((state) => state.assets);
const theme = canvasThemes[rawTheme as keyof typeof canvasThemes] ?? canvasThemes.dark;
const containerRef = useRef<HTMLDivElement | null>(null);

View File

@@ -18,7 +18,7 @@ import { generationTaskShowsProgress, generationTaskStageLabel } from "@/lib/gen
import { navigateToSettings } from "@/lib/settings-navigation";
import { canvasThemes } from "@/lib/canvas-theme";
import { useEffectiveConfig } from "@/stores/use-config-store";
import { useThemeStore } from "@/stores/use-theme-store";
import { useActiveTheme } from "@/stores/canvas/use-canvas-theme-store";
import { STORYBOARD_COMPOSER_MIN_HEIGHT, STORYBOARD_HEADER_HEIGHT, STORYBOARD_ROW_HEIGHT, storyboardTableHeight } from "@/lib/canvas/canvas-storyboard-layout";
import type {
CanvasGenerationBatch,
@@ -135,7 +135,7 @@ export function CanvasScriptNodeContent({
onScrollTopChange: (scrollTop: number) => void;
workspaceMode?: CanvasWorkspaceMode;
}) {
const theme = canvasThemes[useThemeStore((state) => state.theme)];
const theme = canvasThemes[useActiveTheme()];
const effectiveConfig = useEffectiveConfig();
const generationConfig = buildGenerationConfig(effectiveConfig, node, "text");
const simpleMode = workspaceMode === "simple";

View File

@@ -4,11 +4,11 @@ import { Copy, Link2, RefreshCw, Share2, Unlink } from "lucide-react";
import { canvasThemes } from "@/lib/canvas-theme";
import { createCanvasShare, deleteCanvasShare, getCanvasShare, type CanvasShareStatus } from "@/services/api/canvas-share";
import { useThemeStore } from "@/stores/use-theme-store";
import { useActiveTheme } from "@/stores/canvas/use-canvas-theme-store";
export function CanvasShareModal({ projectId, open, onClose, beforeCreate }: { projectId: string; open: boolean; onClose: () => void; beforeCreate: () => Promise<boolean | void> }) {
const { message, modal } = App.useApp();
const theme = canvasThemes[useThemeStore((state) => state.theme)];
const theme = canvasThemes[useActiveTheme()];
const [share, setShare] = useState<CanvasShareStatus>({ enabled: false });
const [expiresDays, setExpiresDays] = useState(0);
const [loading, setLoading] = useState(false);

View File

@@ -5,11 +5,11 @@ import { AlignLeft, ArrowRight, Bot, Check, ChevronDown, ChevronUp, Clapperboard
import { canvasThemes } from "@/lib/canvas-theme";
import { CanvasCreateMenu, type CanvasCreateCommand } from "@/components/canvas/canvas-create-menu";
import type { CanvasShortDramaProgress, CanvasShortDramaStepId } from "@/lib/canvas/canvas-short-drama";
import { useThemeStore } from "@/stores/use-theme-store";
import { useActiveTheme } from "@/stores/canvas/use-canvas-theme-store";
import type { CanvasNodeData } from "@/types/canvas";
export function CanvasLinkedProjectEmptyState({ projectName, hasChapter, onAddFirstChapter, onOpenAssets, onAddText }: { projectName: string; hasChapter: boolean; onAddFirstChapter: () => void; onOpenAssets: () => void; onAddText: () => void }) {
const theme = canvasThemes[useThemeStore((state) => state.theme)];
const theme = canvasThemes[useActiveTheme()];
return (
<div className="pointer-events-none absolute inset-0 z-20 grid place-items-center px-4 pb-16 pt-20">
<div className="pointer-events-auto w-full max-w-[440px] rounded-lg border p-3 shadow-sm backdrop-blur" data-canvas-no-zoom style={{ background: theme.toolbar.panel, borderColor: theme.toolbar.border, color: theme.node.text }}>
@@ -32,7 +32,7 @@ export function CanvasShortDramaEmptyState({ onCreatePipeline, onOpenAgent, onSt
onAddText: () => void;
onAddScript: () => void;
}) {
const theme = canvasThemes[useThemeStore((state) => state.theme)];
const theme = canvasThemes[useActiveTheme()];
const focusStyle = { "--tw-ring-color": theme.accent.primary } as CSSProperties;
return (
<div className="pointer-events-none absolute inset-0 z-20 grid place-items-center px-4 pb-20 pt-24">
@@ -95,7 +95,7 @@ export function CanvasShortDramaEmptyState({ onCreatePipeline, onOpenAgent, onSt
}
export function CanvasFreeformEmptyState({ commands }: { commands: CanvasCreateCommand[] }) {
const theme = canvasThemes[useThemeStore((state) => state.theme)];
const theme = canvasThemes[useActiveTheme()];
const [createOpen, setCreateOpen] = useState(false);
const createCommands = commands.map((command) => ({
...command,
@@ -161,7 +161,7 @@ export function CanvasShortDramaGuide({ progress, collapsed, onToggle, onSkip, o
onSkip: () => void;
onStepClick: (stepId: CanvasShortDramaStepId) => void;
}) {
const theme = canvasThemes[useThemeStore((state) => state.theme)];
const theme = canvasThemes[useActiveTheme()];
if (!progress.active || collapsed) return null;
return (
<div data-canvas-no-zoom className="absolute left-1/2 top-[var(--canvas-topbar-offset)] z-[var(--z-toolbar)] flex max-w-[calc(100%_-_24px)] -translate-x-1/2 items-center gap-1 rounded-lg border p-1 shadow-sm backdrop-blur" style={{ background: theme.toolbar.panel, borderColor: theme.toolbar.border, color: theme.node.text }}>
@@ -216,7 +216,7 @@ export function CanvasShortDramaGuide({ progress, collapsed, onToggle, onSkip, o
}
export function CanvasStylePlaceholderNodeContent({ onChoose }: { onChoose: () => void }) {
const theme = canvasThemes[useThemeStore((state) => state.theme)];
const theme = canvasThemes[useActiveTheme()];
return (
<div className="flex h-full w-full flex-col items-center justify-center px-6 text-center" style={{ color: theme.node.text }}>
<span className="grid size-10 place-items-center rounded-md" style={{ background: `${theme.accent.primary}16`, color: theme.accent.primary }}><Palette className="size-5" /></span>
@@ -228,7 +228,7 @@ export function CanvasStylePlaceholderNodeContent({ onChoose }: { onChoose: () =
}
export function CanvasStoryInputNodeContent({ node, onEdit }: { node: CanvasNodeData; onEdit: () => void }) {
const theme = canvasThemes[useThemeStore((state) => state.theme)];
const theme = canvasThemes[useActiveTheme()];
const content = (node.metadata?.content || "").replace(/\s+/g, " ").trim();
return (
<div className="flex h-full w-full flex-col overflow-hidden p-4" style={{ color: theme.node.text }}>

View File

@@ -16,7 +16,7 @@ import {
type ProjectStyleSelection,
} from "@/lib/canvas/canvas-style-system";
import { createStyleProfile, deleteStyleProfile, listStyleProfiles, setStyleProfileFavorite, touchStyleProfile, updateStyleProfile, type UserStyleProfile } from "@/services/api/style-profiles";
import { useThemeStore } from "@/stores/use-theme-store";
import { useActiveTheme } from "@/stores/canvas/use-canvas-theme-store";
export type { CanvasStylePreset } from "@/lib/canvas/canvas-style-system";
@@ -437,7 +437,7 @@ export function CanvasStylePickerModal({ open, value, currentProfile, startInEdi
}) {
const { message, modal } = App.useApp();
const queryClient = useQueryClient();
const theme = canvasThemes[useThemeStore((state) => state.theme)];
const theme = canvasThemes[useActiveTheme()];
const [detailPreset, setDetailPreset] = useState<CanvasStylePreset | null>(null);
const [tab, setTab] = useState<StyleCenterTab>("system");
const [query, setQuery] = useState("");
@@ -670,7 +670,7 @@ function editableCopy(profile: StyleProfileSnapshot, entityId?: string, title?:
}
export function CanvasStyleDetailModal({ open, preset, selected = false, onClose, onSelect }: { open: boolean; preset: CanvasStylePreset | null; selected?: boolean; onClose: () => void; onSelect?: (preset: CanvasStylePreset) => void }) {
const theme = canvasThemes[useThemeStore((state) => state.theme)];
const theme = canvasThemes[useActiveTheme()];
const sections = preset ? parseStyleSections(preset.prompt) : [];
return (
<AppModal rootClassName="canvas-style-detail-modal" open={open} title={null} footer={null} centered destroyOnHidden width="min(820px, calc(100vw - 24px))" onCancel={onClose} flush>

View File

@@ -6,7 +6,7 @@ import { Captions, FileDown, FileUp, ListPlus, LoaderCircle, Plus, Scissors, Spa
import { saveAs } from "file-saver";
import { canvasThemes } from "@/lib/canvas-theme";
import { useThemeStore } from "@/stores/use-theme-store";
import { useActiveTheme } from "@/stores/canvas/use-canvas-theme-store";
import { useConfigStore, type AiConfig } from "@/stores/use-config-store";
import { resolveMediaUrl } from "@/services/file-storage";
import { cacheResourceObjectUrl } from "@/services/resource-blob-cache";
@@ -30,7 +30,7 @@ type CanvasSubtitleDialogProps = {
export function CanvasSubtitleDialog({ node, open, projectId, config, onClose, onSave }: CanvasSubtitleDialogProps) {
const { message, modal } = App.useApp();
const theme = canvasThemes[useThemeStore((state) => state.theme)];
const theme = canvasThemes[useActiveTheme()];
const isAiConfigReady = useConfigStore((state) => state.isAiConfigReady);
const [entries, setEntries] = useState<SrtEntry[]>([]);
const [highlights, setHighlights] = useState<SubtitleHighlight[]>([]);

View File

@@ -32,7 +32,7 @@ import {
import { canvasThemes } from "@/lib/canvas-theme";
import { createCanvasRichTextExtensions, isSafeCanvasRichTextLink } from "@/lib/canvas/canvas-rich-text";
import { useThemeStore } from "@/stores/use-theme-store";
import { useActiveTheme } from "@/stores/canvas/use-canvas-theme-store";
import type { CanvasNodeData } from "@/types/canvas";
type CanvasTextEditorModalProps = {
@@ -44,7 +44,7 @@ type CanvasTextEditorModalProps = {
export function CanvasTextEditorModal({ node, open, onClose, onSave }: CanvasTextEditorModalProps) {
const { message, modal } = App.useApp();
const theme = canvasThemes[useThemeStore((state) => state.theme)];
const theme = canvasThemes[useActiveTheme()];
const [title, setTitle] = useState("");
const [dirty, setDirty] = useState(false);
const [saving, setSaving] = useState(false);
@@ -161,7 +161,7 @@ export function CanvasTextEditorModal({ node, open, onClose, onSave }: CanvasTex
function TextEditorToolbar({ editor }: { editor: Editor | null }) {
const { message } = App.useApp();
const theme = canvasThemes[useThemeStore((state) => state.theme)];
const theme = canvasThemes[useActiveTheme()];
const [, setToolbarVersion] = useState(0);
const [linkOpen, setLinkOpen] = useState(false);
const [linkValue, setLinkValue] = useState("");
@@ -276,7 +276,7 @@ function TextEditorToolbar({ editor }: { editor: Editor | null }) {
}
function EditorToolButton({ label, active = false, children, onClick }: { label: string; active?: boolean; children: ReactNode; onClick: () => void }) {
const theme = canvasThemes[useThemeStore((state) => state.theme)];
const theme = canvasThemes[useActiveTheme()];
return (
<Tooltip title={label}>
<button type="button" aria-label={label} aria-pressed={active} className="grid size-8 shrink-0 place-items-center rounded-md outline-none transition hover:bg-black/5 focus-visible:ring-2 dark:hover:bg-white/8 [&_svg]:size-3.5" style={{ background: active ? theme.toolbar.activeBg : undefined, color: active ? theme.accent.primary : undefined }} onClick={onClick}>{children}</button>

View File

@@ -13,7 +13,7 @@ import { saveAs } from "file-saver";
import { CanvasTimelineRuler } from "./canvas-timeline-ruler";
import { CanvasTimelinePreview } from "./canvas-timeline-preview";
import { canvasThemes } from "@/lib/canvas-theme";
import { useThemeStore } from "@/stores/use-theme-store";
import { useActiveTheme } from "@/stores/canvas/use-canvas-theme-store";
import { buildTimelineFromNodes, isNodeInTimeline, syncTimelineSubtitleClips } from "@/lib/timeline/timeline-build";
import { canPlaceAt, clampClipDurationByNeighbors, findNearestAvailablePlacement } from "@/lib/timeline/timeline-placement";
import { computeSnap } from "@/lib/timeline/timeline-snap";
@@ -77,7 +77,7 @@ export function CanvasTimelineDialog({
onCreateAssembledNode,
}: CanvasTimelineDialogProps) {
const { message } = App.useApp();
const theme = canvasThemes[useThemeStore((state) => state.theme)];
const theme = canvasThemes[useActiveTheme()];
const [draft, setDraft] = useState<TimelineProject>(() => buildTimelineFromNodes([]));
const [playheadMs, setPlayheadMs] = useState(0);
const [zoomLevel, setZoomLevel] = useState(1);

View File

@@ -15,7 +15,7 @@ import { canvasDockStyle } from "@/lib/canvas/canvas-aceternity-style";
import type { CanvasAppearance } from "@/lib/canvas/canvas-appearance";
import { canvasThemes, type CanvasBackgroundMode, type CanvasTheme } from "@/lib/canvas-theme";
import { defaultToolbarPrefs, readToolbarPrefs, resolveToolbarEntries, type ToolContext, type ToolbarHandlers, type ToolbarPrefs } from "@/lib/canvas/tool-registry";
import { useThemeStore } from "@/stores/use-theme-store";
import { useActiveTheme } from "@/stores/canvas/use-canvas-theme-store";
import type { CanvasNodeTypeId, CanvasToolMode, CanvasWorkspaceMode } from "@/types/canvas";
export function CanvasToolbar({
@@ -92,7 +92,7 @@ export function CanvasToolbar({
const rootRef = useRef<HTMLDivElement>(null);
const { bringToFront, zIndex } = useCanvasOverlayLayer("main-toolbar", "var(--z-toolbar)");
const dockRef = useRef<HTMLDivElement>(null);
const colorTheme = useThemeStore((state) => state.theme);
const colorTheme = useActiveTheme();
const theme = canvasThemes[colorTheme];
const [addOpen, setAddOpen] = useState(false);
const [appearanceOpen, setAppearanceOpen] = useState(false);

View File

@@ -3,11 +3,11 @@ import { Check, Star } from "lucide-react";
import { canvasThemes } from "@/lib/canvas-theme";
import { canvasNodeVideoPreviewUrl } from "@/lib/canvas/canvas-media-preview";
import { useThemeStore } from "@/stores/use-theme-store";
import { useActiveTheme } from "@/stores/canvas/use-canvas-theme-store";
import { CanvasNodeType, type CanvasNodeData } from "@/types/canvas";
export function CanvasVersionCompareModal({ open, versions, onClose, onSetPrimary, onFocus }: { open: boolean; versions: CanvasNodeData[]; onClose: () => void; onSetPrimary: (nodeId: string) => void; onFocus: (nodeId: string) => void }) {
const theme = canvasThemes[useThemeStore((state) => state.theme)];
const theme = canvasThemes[useActiveTheme()];
const modalWidth = Math.min(1180, Math.max(440, 112 + versions.length * 340));
return (
<Modal title="版本对比" open={open} footer={null} width={modalWidth} centered onCancel={onClose} styles={{ body: { overflow: "hidden" } }}>

View File

@@ -8,7 +8,7 @@ import { formatVideoFrameTime, normalizeVideoFrameTimes } from "@/lib/canvas/can
import { resourceIdFromStorageKey } from "@/services/api/resources";
import { resolveMediaUrl } from "@/services/file-storage";
import { cacheResourceObjectUrl } from "@/services/resource-blob-cache";
import { useThemeStore } from "@/stores/use-theme-store";
import { useActiveTheme } from "@/stores/canvas/use-canvas-theme-store";
import type { CanvasNodeData } from "@/types/canvas";
type SelectedVideoFrame = {
@@ -31,7 +31,7 @@ const MAX_SELECTED_FRAMES = 30;
export function CanvasVideoFrameDialog({ node, open, onClose, onConfirm }: CanvasVideoFrameDialogProps) {
const { message } = App.useApp();
const theme = canvasThemes[useThemeStore((state) => state.theme)];
const theme = canvasThemes[useActiveTheme()];
const videoRef = useRef<HTMLVideoElement>(null);
const [videoUrl, setVideoUrl] = useState("");
const [videoError, setVideoError] = useState(false);

View File

@@ -3,7 +3,7 @@ import { createPortal } from "react-dom";
import { Check, ChevronDown, Image as ImageIcon } from "lucide-react";
import { canvasThemes, type CanvasTheme } from "@/lib/canvas-theme";
import { useThemeStore } from "@/stores/use-theme-store";
import { useActiveTheme } from "@/stores/canvas/use-canvas-theme-store";
import type { CanvasNodeMetadata } from "@/types/canvas";
type VideoFrameOption = {
@@ -34,7 +34,7 @@ const MENU_ITEM_HEIGHT = 28;
const CONTROL_TEXT_STYLE: CSSProperties = { fontFamily: "inherit", fontSize: 11, fontWeight: 400, letterSpacing: 0, lineHeight: 1 };
export function CanvasVideoPromptTools({ metadata, frameOptions, onMetadataChange, referenceMode = "frames", referenceSummary }: CanvasVideoPromptToolsProps) {
const theme = canvasThemes[useThemeStore((state) => state.theme)];
const theme = canvasThemes[useActiveTheme()];
const startFrame = metadata?.videoStartFrameNodeId || EMPTY_FRAME_VALUE;
const endFrame = metadata?.videoEndFrameNodeId || EMPTY_FRAME_VALUE;

View File

@@ -10,7 +10,7 @@ import { listVideoReferenceModels } from "@/lib/canvas/canvas-video-regeneration
import { modelCapabilityConfigFor } from "@/lib/model-capabilities";
import { modelRequestOptions, resolveCompatibleModel, type ModelRequirements } from "@/lib/model-selection";
import { navigateToSettings } from "@/lib/settings-navigation";
import { useThemeStore } from "@/stores/use-theme-store";
import { useActiveTheme } from "@/stores/canvas/use-canvas-theme-store";
import { resolveMediaUrl } from "@/services/file-storage";
import { cacheResourceObjectUrl } from "@/services/resource-blob-cache";
import { resourceIdFromStorageKey } from "@/services/api/resources";
@@ -47,7 +47,7 @@ const MIN_SEGMENT_MS = 100;
export function CanvasVideoSegmentDialog({ node, nodes, connections, open, mode, config, timeline, onClose, onConfirm }: CanvasVideoSegmentDialogProps) {
const { message } = App.useApp();
const theme = canvasThemes[useThemeStore((state) => state.theme)];
const theme = canvasThemes[useActiveTheme()];
const videoRef = useRef<HTMLVideoElement>(null);
const segmentsSeededRef = useRef(false);
const [videoUrl, setVideoUrl] = useState("");

View File

@@ -6,7 +6,7 @@ import { Button } from "antd";
import { VideoSettingsPanel, videoResolutionLabel, videoSecondsLabel, videoSizeLabel } from "@/components/video-settings-panel";
import { canvasThemes } from "@/lib/canvas-theme";
import { modelCapabilityConfigFor, resolveVideoRatioValue, resolveVideoResolutionValue } from "@/lib/model-capabilities";
import { useThemeStore } from "@/stores/use-theme-store";
import { useActiveTheme } from "@/stores/canvas/use-canvas-theme-store";
import type { AiConfig } from "@/stores/use-config-store";
type CanvasVideoSettingsPopoverProps = {
@@ -17,7 +17,7 @@ type CanvasVideoSettingsPopoverProps = {
};
export function CanvasVideoSettingsPopover({ config, onConfigChange, buttonClassName, placement = "topLeft" }: CanvasVideoSettingsPopoverProps) {
const theme = canvasThemes[useThemeStore((state) => state.theme)];
const theme = canvasThemes[useActiveTheme()];
const buttonRef = useRef<HTMLSpanElement>(null);
const panelRef = useRef<HTMLDivElement>(null);
const [open, setOpen] = useState(false);

View File

@@ -6,7 +6,7 @@ import { useCanvasOverlayLayer } from "@/components/canvas/canvas-overlay-layer"
import { canvasThemes } from "@/lib/canvas-theme";
import { aceternityMotion } from "@/lib/aceternity-motion";
import { subscribeCanvasGraphicsViewportPreview, subscribeCanvasNodeDragPreview, subscribeCanvasViewportPreview } from "@/lib/canvas/canvas-live-viewport";
import { useThemeStore } from "@/stores/use-theme-store";
import { useActiveTheme } from "@/stores/canvas/use-canvas-theme-store";
import { CanvasNodeType, type CanvasNodeData, type ConnectionHandle, type Position, type ViewportTransform } from "@/types/canvas";
export type PendingConnectionCreate = {
@@ -17,7 +17,7 @@ export type PendingConnectionCreate = {
};
export function CanvasSelectionToolbar({ anchorRef, containerRef, count, children }: { anchorRef: RefObject<HTMLDivElement | null>; containerRef: RefObject<HTMLDivElement | null>; count: number; children: ReactNode }) {
const theme = canvasThemes[useThemeStore((state) => state.theme)];
const theme = canvasThemes[useActiveTheme()];
const reducedMotion = useReducedMotion();
const toolbarRef = useRef<HTMLDivElement>(null);
const [anchor, setAnchor] = useState<{ left: number; top: number; placement: "above" | "below" } | null>(null);
@@ -155,7 +155,7 @@ function resolveNodePanelWidth(node: CanvasNodeData, viewport: ViewportTransform
}
export function CanvasConnectionCreateMenu({ pending, viewport, viewportSize, containerRef, canCreateDrawing, getDisabledReason, onCreate, onClose }: { pending: PendingConnectionCreate; viewport: ViewportTransform; viewportSize: { width: number; height: number }; containerRef: RefObject<HTMLDivElement | null>; canCreateDrawing: boolean; getDisabledReason: (type: CanvasNodeType.Image | CanvasNodeType.Text | CanvasNodeType.Script | CanvasNodeType.BatchTable | CanvasNodeType.Video | CanvasNodeType.Audio | CanvasNodeType.Drawing | CanvasNodeType.Config | CanvasNodeType.MediaConversion, provider?: "runninghub") => string; onCreate: (type: CanvasNodeType.Image | CanvasNodeType.Text | CanvasNodeType.Script | CanvasNodeType.BatchTable | CanvasNodeType.Video | CanvasNodeType.Audio | CanvasNodeType.Drawing | CanvasNodeType.Config | CanvasNodeType.MediaConversion, provider?: "runninghub") => void; onClose: () => void }) {
const theme = canvasThemes[useThemeStore((state) => state.theme)];
const theme = canvasThemes[useActiveTheme()];
const reducedMotion = useReducedMotion();
const menuRef = useRef<HTMLDivElement>(null);
const [activeOption, setActiveOption] = useState<string | null>(null);
@@ -241,7 +241,7 @@ export function CanvasConnectionCreateMenu({ pending, viewport, viewportSize, co
}
function ConnectionCreateOption({ expanded, motionEnabled, icon, title, description, disabledReason, onClick }: { expanded: boolean; motionEnabled: boolean; icon: ReactNode; title: string; description: string; disabledReason?: string; onClick: () => void }) {
const theme = canvasThemes[useThemeStore((state) => state.theme)];
const theme = canvasThemes[useActiveTheme()];
return (
<button type="button" aria-disabled={Boolean(disabledReason)} aria-label={title} aria-description={disabledReason || description} data-create-option={title} data-expanded={expanded} data-motion={motionEnabled ? "enabled" : "reduced"} className="canvas-connection-create-option group flex min-h-10 w-full cursor-pointer items-start gap-2 rounded-[var(--dock-item-radius)] px-2 py-1.5 text-left outline-none focus-visible:ring-2 aria-disabled:cursor-not-allowed aria-disabled:opacity-40" style={{ color: theme.node.text, "--tw-ring-color": theme.node.muted, background: expanded ? theme.toolbar.itemHover : undefined } as CSSProperties} onClick={() => { if (!disabledReason) onClick(); }}>
<span className="grid size-7 shrink-0 place-items-center rounded-[var(--r-md)] opacity-65 transition-opacity group-hover:opacity-100 [&_svg]:size-3.5" style={{ background: theme.toolbar.itemHover }}>{icon}</span>

View File

@@ -7,7 +7,7 @@ import { aceternityMotion } from "@/lib/aceternity-motion";
import { canvasDockStyle } from "@/lib/canvas/canvas-aceternity-style";
import { canvasThemes } from "@/lib/canvas-theme";
import { subscribeCanvasViewportPreview } from "@/lib/canvas/canvas-live-viewport";
import { useThemeStore } from "@/stores/use-theme-store";
import { useActiveTheme } from "@/stores/canvas/use-canvas-theme-store";
type CanvasZoomControlsProps = {
scale: number;
@@ -23,7 +23,7 @@ type CanvasZoomControlsProps = {
const QUICK_ZOOM_LEVELS = [0.25, 0.5, 1, 2] as const;
export function CanvasZoomControls({ scale, onScaleChange, onFitContent, onAutoArrange, isMiniMapOpen, onToggleMiniMap, onOpenShortcuts, containerRef }: CanvasZoomControlsProps) {
const theme = canvasThemes[useThemeStore((state) => state.theme)];
const theme = canvasThemes[useActiveTheme()];
const rootRef = useRef<HTMLDivElement>(null);
const liveScaleRef = useRef(scale);
const rangeRef = useRef<HTMLInputElement>(null);

View File

@@ -3,12 +3,12 @@ import { Box, Camera, Clapperboard, Lightbulb, LockKeyhole, Move3d } from "lucid
import { canvasThemes, type CanvasTheme } from "@/lib/canvas-theme";
import { gateDirectorPreviewFailure, resolveDirectorActiveShot, resolveDirectorPreviewSource, type DirectorNodeContentReader } from "@/lib/canvas/director/director-preview";
import { useThemeStore } from "@/stores/use-theme-store";
import { useActiveTheme } from "@/stores/canvas/use-canvas-theme-store";
import type { CanvasNodeData } from "@/types/canvas";
import type { DirectorScene } from "@/types/director";
export function CanvasDirectorNodePanel({ node, scene, readNodeContent, onOpen, professional = true }: { node: CanvasNodeData; scene: DirectorScene | null; readNodeContent: DirectorNodeContentReader; onOpen: () => void; professional?: boolean }) {
const theme = canvasThemes[useThemeStore((state) => state.theme)];
const theme = canvasThemes[useActiveTheme()];
const shot = resolveDirectorActiveShot(scene, node.metadata?.directorShotId);
// 记录「失败的那个 URL」而非布尔量:同一个坏 URL 不再反复渲染,换成另一个 URL 时自动重试。
const [failedUrl, setFailedUrl] = useState<string | null>(null);

View File

@@ -27,13 +27,13 @@ import { uploadMediaFile } from "@/services/file-storage";
import { localSavedRemotePendingMessage, saveRemoteUserDataNow } from "@/services/user-data-sync";
import { useAssetStore, type ModelAsset } from "@/stores/use-asset-store";
import { useDirectorWorkbenchStore } from "@/stores/canvas/use-director-workbench-store";
import { useThemeStore } from "@/stores/use-theme-store";
import { useActiveTheme } from "@/stores/canvas/use-canvas-theme-store";
import type { CanvasNodeData } from "@/types/canvas";
import type { DirectorCamera, DirectorCameraMove, DirectorHumanoidBone, DirectorKeyframeDeleteTarget, DirectorKeyframeEasing, DirectorLight, DirectorObject, DirectorPose, DirectorQuat, DirectorRenderMode, DirectorRig, DirectorScene, DirectorSceneOutput, DirectorShot, DirectorShotSize, DirectorTransform, DirectorVec3 } from "@/types/director";
export function CanvasDirectorWorkbench({ open, scene, imageNodes, onboardingScope, onClose, onChange, onApply, onDeleteImageNode, onFlush }: { open: boolean; scene: DirectorScene | null; imageNodes: CanvasNodeData[]; onboardingScope: string; onClose: () => void; onChange: (scene: DirectorScene) => void; onApply: (output: DirectorSceneOutput) => Promise<void>; onDeleteImageNode: (nodeId: string) => void; onFlush?: () => void | Promise<void> }) {
const { message, modal } = App.useApp();
const theme = canvasThemes[useThemeStore((state) => state.theme)];
const theme = canvasThemes[useActiveTheme()];
const viewportRef = useRef<DirectorViewportHandle>(null);
const modelInputRef = useRef<HTMLInputElement>(null);
const [draft, setDraft] = useState<DirectorScene | null>(null);

View File

@@ -4,7 +4,7 @@ import { resolveCanvasAppearance, resolveCanvasGridColor, type CanvasAppearance
import { resolveCanvasPointerIntent } from "@/lib/canvas/canvas-selection";
import type { CanvasBackgroundMode } from "@/lib/canvas-theme";
import { applyCanvasLiveViewport, subscribeCanvasViewportPreview } from "@/lib/canvas/canvas-live-viewport";
import { useThemeStore } from "@/stores/use-theme-store";
import { useActiveTheme } from "@/stores/canvas/use-canvas-theme-store";
import type { ViewportTransform } from "@/types/canvas";
type InfiniteCanvasProps = {
@@ -44,7 +44,7 @@ type PinchState = {
};
export function InfiniteCanvas({ containerRef, viewport, appearance, backgroundMode = "lines", onViewportChange, onViewportPreviewChange, onCanvasMouseDown, boxSelectEnabled = false, onCanvasDoubleClick, onCanvasDeselect, onContextMenu, onDrop, onFileDragEnter, onFileDragLeave, onFileDragOver, graphicsLayer, children }: InfiniteCanvasProps) {
const colorTheme = useThemeStore((state) => state.theme);
const colorTheme = useActiveTheme();
const resolvedAppearance = resolveCanvasAppearance(appearance, colorTheme);
const panState = useRef({
isPanning: false,
@@ -439,7 +439,7 @@ export function InfiniteCanvas({ containerRef, viewport, appearance, backgroundM
}
function CanvasGrid({ appearance, mode }: { appearance?: CanvasAppearance; mode: CanvasBackgroundMode }) {
const colorTheme = useThemeStore((state) => state.theme);
const colorTheme = useActiveTheme();
const gridColor = resolveCanvasGridColor(appearance, colorTheme, mode);
const backgroundImage = mode === "dots" ? `radial-gradient(circle, ${gridColor} 0.8px, transparent 1px)` : `linear-gradient(${gridColor} 1px, transparent 1px), linear-gradient(90deg, ${gridColor} 1px, transparent 1px)`;
if (mode === "blank") return null;

View File

@@ -3,7 +3,7 @@ import { ArrowRight, CheckCircle2, CircleAlert, Image as ImageIcon, LoaderCircle
import { canvasThemes } from "@/lib/canvas-theme";
import { ART_CRITIQUE_PLUGIN_ID, artCritiqueSourceFingerprint, artCritiqueStageLabel, createDefaultArtCritiqueState, isArtCritiqueImageInput, type ArtCritiqueNodeState } from "@/lib/art-critique/contracts";
import { usePluginStore } from "@/stores/use-plugin-store";
import { useThemeStore } from "@/stores/use-theme-store";
import { useActiveTheme } from "@/stores/canvas/use-canvas-theme-store";
import { CanvasNodeType, type CanvasNodeData } from "@/types/canvas";
import { useCanvasNodeActions } from "../canvas-node-action-context";
@@ -14,7 +14,7 @@ type ArtCritiqueNodeProps = {
};
export function ArtCritiqueNodeContent({ node }: ArtCritiqueNodeProps) {
const theme = canvasThemes[useThemeStore((state) => state.theme)];
const theme = canvasThemes[useActiveTheme()];
const installations = usePluginStore((state) => state.installations);
const { openArtCritique } = useCanvasNodeActions();
const imageInputs = useUpstreamNodes(node.id).filter(isArtCritiqueImageInput);

View File

@@ -6,7 +6,7 @@ import { useEffect, useRef, useState } from "react";
import { canvasThemes } from "@/lib/canvas-theme";
import { defaultToolbarPrefs, getToolbarTools, persistToolbarPrefs, readToolbarPrefs, type ToolbarId, type ToolbarPrefs, type ToolContext, type ToolDefinition } from "@/lib/canvas/tool-registry";
import { useThemeStore } from "@/stores/use-theme-store";
import { useActiveTheme } from "@/stores/canvas/use-canvas-theme-store";
type ToolbarSettingsModalProps = {
open: boolean;
@@ -42,7 +42,7 @@ type SettingsItem = {
};
export function ToolbarSettingsModal({ open, onClose, toolbar }: ToolbarSettingsModalProps) {
const theme = canvasThemes[useThemeStore((state) => state.theme)];
const theme = canvasThemes[useActiveTheme()];
const reducedMotion = useReducedMotion();
const [items, setItems] = useState<SettingsItem[]>([]);
const [toolbarId, setToolbarId] = useState<ToolbarId>(toolbar);

View File

@@ -4,23 +4,25 @@ import { useState } from "react";
import { Mic } from "lucide-react";
import { cn } from "@/lib/utils";
import { VoiceRecordingInline } from "./voice-recording-inline";
import { canvasThemes } from "@/lib/canvas-theme";
import { useThemeStore } from "@/stores/use-theme-store";
import { useActiveTheme } from "@/stores/canvas/use-canvas-theme-store";
type VoiceRecordingButtonProps = {
/** 转写完成回调,返回转写文本 */
onTranscribed: (text: string) => void;
/** 是否禁用(如发送中或未连接) */
disabled?: boolean;
className?: string;
};
/**
* 语音输入按钮:点击后在输入行内展开波形录制条,录制完成自动 STT 转写
* 使用局部状态,多个输入行可独立使用
*/
export function VoiceRecordingButton({ onTranscribed, disabled }: VoiceRecordingButtonProps) {
const theme = canvasThemes[useThemeStore((state) => state.theme)];
export function VoiceRecordingButton({ onTranscribed, disabled, className }: VoiceRecordingButtonProps) {
const theme = canvasThemes[useActiveTheme()];
const [open, setOpen] = useState(false);
return (
@@ -29,9 +31,9 @@ export function VoiceRecordingButton({ onTranscribed, disabled }: VoiceRecording
<Button
type="text"
shape="circle"
className="!h-8 !w-8 !min-w-8"
className={cn("!h-8 !w-8 !min-w-8", className)}
disabled={disabled}
style={{ color: theme.node.muted }}
style={className ? undefined : { color: theme.node.muted }}
icon={<Mic className="size-4" />}
onClick={() => setOpen(true)}
aria-label="实时对话"
@@ -48,4 +50,4 @@ export function VoiceRecordingButton({ onTranscribed, disabled }: VoiceRecording
) : null}
</>
);
}
}

View File

@@ -8,7 +8,7 @@ import { AudioWaveform } from "./audio-waveform";
import { useSpeechRecognition } from "@/hooks/use-speech-recognition";
import { useVoiceRecording } from "@/hooks/use-voice-recording";
import { canvasThemes } from "@/lib/canvas-theme";
import { useThemeStore } from "@/stores/use-theme-store";
import { useActiveTheme } from "@/stores/canvas/use-canvas-theme-store";
type VoiceRecordingInlineProps = {
/** 转写完成回调,返回转写文本 */
@@ -24,7 +24,7 @@ type TranscribeState = "idle" | "transcribing" | "done" | "error";
* 挂载后自动开始录音,显示波形动画;点击停止后自动转写为文字(浏览器 Web Speech API,无需后端与 API Key)
*/
export function VoiceRecordingInline({ onTranscribed, onCancel }: VoiceRecordingInlineProps) {
const theme = canvasThemes[useThemeStore((state) => state.theme)];
const theme = canvasThemes[useActiveTheme()];
const {
state,
waveform,

View File

@@ -1,5 +1,5 @@
import type { ReactNode } from "react";
import { lazy, Suspense, useEffect } from "react";
import { lazy, Suspense, useLayoutEffect } from "react";
import { QueryClientProvider } from "@tanstack/react-query";
import { App, ConfigProvider } from "antd";
import zhCN from "antd/locale/zh_CN";
@@ -9,7 +9,7 @@ import { FullScreenLoader } from "@/components/ui/aceternity/full-screen-loader"
import { getAntThemeConfig } from "@/lib/app-theme";
import { applySkinTheme } from "@/lib/skin-themes";
import { appQueryClient } from "@/lib/query-client";
import { useThemeStore } from "@/stores/use-theme-store";
import { useActiveTheme } from "@/stores/canvas/use-canvas-theme-store";
import { applyAppearanceMetadata, useAppearanceStore } from "@/stores/use-appearance-store";
import { useUserStore } from "@/stores/use-user-store";
@@ -22,11 +22,11 @@ function ClientRootBoundary({ children }: { children: ReactNode }) {
}
export function AppProviders({ children }: { children: ReactNode }) {
const theme = useThemeStore((state) => state.theme);
const theme = useActiveTheme();
const dark = theme === "dark";
const appearance = useAppearanceStore((state) => state.appearance);
useEffect(() => {
useLayoutEffect(() => {
document.documentElement.classList.toggle("dark", dark);
document.documentElement.style.colorScheme = theme;
applySkinTheme(appearance.activeSkin, theme);

View File

@@ -2,7 +2,7 @@ import CodeMirror, { EditorView } from "@uiw/react-codemirror";
import { forwardRef, useImperativeHandle, useRef } from "react";
import { cn } from "@/lib/utils";
import { useThemeStore } from "@/stores/use-theme-store";
import { useActiveTheme } from "@/stores/canvas/use-canvas-theme-store";
export type PromptCodeEditorHandle = {
insertText: (text: string) => void;
@@ -18,7 +18,7 @@ type PromptCodeEditorProps = {
};
export const PromptCodeEditor = forwardRef<PromptCodeEditorHandle, PromptCodeEditorProps>(function PromptCodeEditor({ value, onChange, readOnly = false, ariaLabel, className }, ref) {
const theme = useThemeStore((state) => state.theme);
const theme = useActiveTheme();
const viewRef = useRef<EditorView | null>(null);
useImperativeHandle(ref, () => ({