feat(canvas): 画布主题 - 与工作台外观隔离并提高连线对比
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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}</>;
|
||||
|
||||
@@ -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) : "未配置文本/视觉理解模型";
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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)` }} />;
|
||||
}
|
||||
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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" }}>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 }}>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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[]>([]);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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" } }}>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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("");
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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, () => ({
|
||||
|
||||
Reference in New Issue
Block a user