fix(canvas): 创作工作区 - 收紧界面密度并改进 Agent 镂空标签
This commit is contained in:
@@ -5,6 +5,11 @@ description: 当前项目已实现功能的手工验证清单与未确认边界
|
||||
|
||||
# 待测试
|
||||
|
||||
## 工作区 100% 缩放下的界面密度
|
||||
|
||||
- 已收紧用户工作区侧栏、顶栏和导航行、画布桌面端 Dock、Agent 内边距及默认浮窗尺寸;Agent 旧的手动调整尺寸仍保留,可点标题栏「恢复 Agent 紧凑窗口」自行重置。技能组合标签维持透明底和彩色描边。
|
||||
- [ ] 在已登录的真实画布和用户工作区以浏览器 100% 缩放验收明暗主题、侧栏展开/折叠、顶部/底部工具栏、Agent 历史与设置、输入区和标签横向滚动;再检查窄屏与触屏下是否截断或遮挡。当前本地独立浏览器会跳转登录页,未将静态检查视为视觉验收。
|
||||
|
||||
## 画布资源断链、前端发布兼容与画布热点
|
||||
|
||||
- 已实现:统一递归识别持久资源字段(含嵌套 `videoPreview`);保存前和历史快照保存时校验当前资源归属/ready 状态,资源缺失返回带路径和 ID 的结构化 409。显式修复保留坏版本历史但要求新版本完整且 revision CAS 成功。后台 `repair-canvas-resources` 默认只读,仅可清除确认缺失的派生视频预览;主媒体问题报告为 unresolved。
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Tooltip } from "@/components/ui/base/tooltip";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState, type ClipboardEvent as ReactClipboardEvent, type CSSProperties, type KeyboardEvent as ReactKeyboardEvent, type PointerEvent as ReactPointerEvent, type ReactNode } from "react";
|
||||
|
||||
import { motion, useReducedMotion } from "motion/react";
|
||||
import { ArrowUp, AtSign, CheckCircle2, ChevronDown, ChevronUp, CircleAlert, CircleDot, Eye, HelpCircle, ImagePlus, ListChecks, LoaderCircle, Pencil, Plus, RotateCcw, Sparkles, Square, X, XCircle } from "lucide-react";
|
||||
import { ArrowLeft, ArrowUp, AtSign, Bookmark, CheckCircle2, ChevronDown, ChevronUp, CircleAlert, CircleDot, Clapperboard, Eye, HelpCircle, ImagePlus, Layers3, ListChecks, LoaderCircle, Palette, Pencil, Plus, RotateCcw, Shapes, Share2, ShoppingBag, Sparkles, Square, X, XCircle } from "lucide-react";
|
||||
|
||||
import { canvasThemes } from "@/lib/canvas-theme";
|
||||
import { AIMessageMarkdown } from "@/components/ai/ai-message-markdown";
|
||||
@@ -595,13 +595,13 @@ export type AgentSceneBucket = {
|
||||
};
|
||||
|
||||
/** 场景分类:与 presets.json 的 scene 字段、技能的 tag 字段共用同一套 key。 */
|
||||
export const AGENT_SCENE_DEFS: Array<{ key: string; label: string }> = [
|
||||
{ key: "frequent", label: "我的常用" },
|
||||
{ key: "drama", label: "短剧故事" },
|
||||
{ key: "ecommerce", label: "广告电商" },
|
||||
{ key: "creative", label: "视觉创意" },
|
||||
{ key: "social", label: "传播社媒" },
|
||||
{ key: "others", label: "其他" },
|
||||
export const AGENT_SCENE_DEFS: Array<{ key: string; label: string; icon: typeof Sparkles }> = [
|
||||
{ key: "frequent", label: "我的常用", icon: Bookmark },
|
||||
{ key: "drama", label: "短剧故事", icon: Clapperboard },
|
||||
{ key: "ecommerce", label: "广告电商", icon: ShoppingBag },
|
||||
{ key: "creative", label: "视觉创意", icon: Palette },
|
||||
{ key: "social", label: "传播社媒", icon: Share2 },
|
||||
{ key: "others", label: "其他", icon: Shapes },
|
||||
];
|
||||
|
||||
export function AgentSceneCapsules({ buckets, installedIds, theme, disabled = false, onPick, onPickSkill }: {
|
||||
@@ -614,7 +614,7 @@ export function AgentSceneCapsules({ buckets, installedIds, theme, disabled = fa
|
||||
}) {
|
||||
// 始终只占一排:默认显示场景分类,点某个场景后在同一排内就地切换内容。
|
||||
const [activeKey, setActiveKey] = useState<string | null>(null);
|
||||
const capsuleClass = "agent-scene-capsule shrink-0 rounded-lg px-3 py-1.5 text-left text-xs focus-visible:outline focus-visible:outline-2";
|
||||
const capsuleClass = "agent-scene-capsule shrink-0";
|
||||
const stop = {
|
||||
onMouseDown: (event: { stopPropagation(): void }) => event.stopPropagation(),
|
||||
onPointerDown: (event: { stopPropagation(): void }) => event.stopPropagation(),
|
||||
@@ -623,12 +623,12 @@ export function AgentSceneCapsules({ buckets, installedIds, theme, disabled = fa
|
||||
const active = activeKey ? visible.find((bucket) => bucket.key === activeKey) || null : null;
|
||||
if (!visible.length) return null;
|
||||
return (
|
||||
<div className="agent-scene-capsules mx-3 mb-2 overflow-hidden rounded-xl" style={{ color: theme.node.text }}>
|
||||
<div className="flex items-start gap-2 px-3 pt-2.5">
|
||||
<Sparkles className="mt-[1px] size-3.5 shrink-0" style={{ color: theme.accent.primary }} />
|
||||
<span className="min-w-0 flex-1 text-xs font-semibold leading-5">{active ? active.label : "技能组合推荐"}</span>
|
||||
<div className="agent-scene-capsules mx-3 mb-2 min-w-0" style={{ color: theme.node.text }}>
|
||||
<div className="agent-scene-capsules-heading">
|
||||
<Sparkles aria-hidden="true" />
|
||||
<span>{active ? active.label : "技能组合推荐"}</span>
|
||||
</div>
|
||||
<div className="agent-scene-capsules-scroll thin-scrollbar flex gap-2 overflow-x-auto px-3 pb-1 pt-2">
|
||||
<div className="agent-scene-capsules-scroll thin-scrollbar flex gap-2 overflow-x-auto px-1 py-2">
|
||||
{active ? (
|
||||
<>
|
||||
<button
|
||||
@@ -636,14 +636,15 @@ export function AgentSceneCapsules({ buckets, installedIds, theme, disabled = fa
|
||||
disabled={disabled}
|
||||
title="返回全部场景"
|
||||
className={capsuleClass}
|
||||
data-scene="back"
|
||||
{...stop}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
setActiveKey(null);
|
||||
}}
|
||||
>
|
||||
<span className="block whitespace-nowrap font-medium">← 全部场景</span>
|
||||
<span className="mt-0.5 block whitespace-nowrap text-[10px] leading-4 opacity-60">返回</span>
|
||||
<ArrowLeft aria-hidden="true" />
|
||||
<span>全部场景</span>
|
||||
</button>
|
||||
{active.presets.map((preset) => {
|
||||
const missing = preset.skillIds.filter((id) => !installedIds.has(id)).length;
|
||||
@@ -654,16 +655,16 @@ export function AgentSceneCapsules({ buckets, installedIds, theme, disabled = fa
|
||||
disabled={disabled}
|
||||
title={preset.rationale}
|
||||
className={capsuleClass}
|
||||
data-scene={active.key}
|
||||
{...stop}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
onPick(preset);
|
||||
}}
|
||||
>
|
||||
<span className="block whitespace-nowrap font-medium">{preset.name}</span>
|
||||
<span className="mt-0.5 block whitespace-nowrap text-[10px] leading-4 opacity-60">
|
||||
{missing > 0 ? `${preset.skillIds.length} 个技能 · ${missing} 个待装` : `${preset.skillIds.length} 个技能`}
|
||||
</span>
|
||||
<Layers3 aria-hidden="true" />
|
||||
<span className="agent-scene-capsule-label">{preset.name}</span>
|
||||
<span className="agent-scene-capsule-meta">{missing > 0 ? `${preset.skillIds.length} 技能 · ${missing} 待装` : `${preset.skillIds.length} 技能`}</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
@@ -676,14 +677,16 @@ export function AgentSceneCapsules({ buckets, installedIds, theme, disabled = fa
|
||||
disabled={disabled}
|
||||
title={skill.description}
|
||||
className={capsuleClass}
|
||||
data-scene={active.key}
|
||||
{...stop}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
onPickSkill(skill);
|
||||
}}
|
||||
>
|
||||
<span className="block max-w-[10rem] truncate font-medium">{skill.skillName}</span>
|
||||
<span className="mt-0.5 block whitespace-nowrap text-[10px] leading-4 opacity-60">{owned}</span>
|
||||
<Sparkles aria-hidden="true" />
|
||||
<span className="agent-scene-capsule-label">{skill.skillName}</span>
|
||||
<span className="agent-scene-capsule-meta">{owned}</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
@@ -691,6 +694,7 @@ export function AgentSceneCapsules({ buckets, installedIds, theme, disabled = fa
|
||||
) : (
|
||||
visible.map((bucket) => {
|
||||
const count = bucket.presets.length + bucket.skills.length;
|
||||
const Icon = AGENT_SCENE_DEFS.find((definition) => definition.key === bucket.key)?.icon || Shapes;
|
||||
return (
|
||||
<button
|
||||
key={bucket.key}
|
||||
@@ -698,20 +702,22 @@ export function AgentSceneCapsules({ buckets, installedIds, theme, disabled = fa
|
||||
disabled={disabled}
|
||||
title={`查看「${bucket.label}」下的技能组合`}
|
||||
className={capsuleClass}
|
||||
data-scene={bucket.key}
|
||||
{...stop}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
setActiveKey(bucket.key);
|
||||
}}
|
||||
>
|
||||
<span className="block whitespace-nowrap font-medium">{bucket.label}</span>
|
||||
<span className="mt-0.5 block whitespace-nowrap text-[10px] leading-4 opacity-60">{count} 项</span>
|
||||
<Icon aria-hidden="true" />
|
||||
<span className="agent-scene-capsule-label">{bucket.label}</span>
|
||||
<span className="agent-scene-capsule-meta">{count} 项</span>
|
||||
</button>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
<div className="px-3 pb-2 text-[10px] opacity-50">选择只对当前会话生效;未装的技能会添加到我的技能库。具体用哪张卡由 Agent 按任务检索。</div>
|
||||
<p className="agent-scene-capsules-note">仅本会话生效 · 缺失技能将加入技能库 · Agent 按任务调用</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState, type CSSProperties, type Dispatch, type SetStateAction } from "react";
|
||||
import { Button, Dropdown, Input } from "antd";
|
||||
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
|
||||
import { ArrowLeft, Check, ChevronRight, CircleDot, Clock3, Download, History, LoaderCircle, MessageSquarePlus, MoveDiagonal2, Settings2, ShieldCheck, Trash2, Sparkles, X } from "lucide-react";
|
||||
import { ArrowLeft, Check, ChevronRight, CircleDot, Clock3, Download, History, LoaderCircle, MessageSquarePlus, MoveDiagonal2, RotateCcw, Settings2, ShieldCheck, Trash2, Sparkles, X } from "lucide-react";
|
||||
import { saveAs } from "file-saver";
|
||||
import { buildAgentDebugExport } from "@/lib/canvas/agent-debug-export";
|
||||
import { markdownPlainText } from "@/lib/markdown-plain-text";
|
||||
@@ -840,6 +840,7 @@ export function CanvasCloudAgentPanel({ canvasId, domainProjectId, nodeCount, re
|
||||
})().catch((cause) => setMessages((current) => appendAgentError(current, `export-${Date.now()}`, cause, "导出失败"))).finally(() => setExporting(false));
|
||||
}}
|
||||
onSettings={() => setView("settings")}
|
||||
onResetLayout={panelLayout.reset}
|
||||
onCollapse={onCollapse}
|
||||
/>
|
||||
{run && connectionStatus !== "connected" ? (
|
||||
@@ -978,7 +979,7 @@ function AgentLauncher({ theme, statusColor, approvalPending, reducedMotion, onO
|
||||
);
|
||||
}
|
||||
|
||||
function AgentHeader({ theme, hasMessages, statusLabel, statusColor, nodeCount, onNew, onHistory, onSettings, onCollapse, onExport, exporting }: { theme: CanvasTheme; hasMessages: boolean; statusLabel: string; statusColor: string; nodeCount: number; onNew: () => void; onHistory: () => void; onSettings: () => void; onCollapse: () => void; onExport: () => void; exporting: boolean }) {
|
||||
function AgentHeader({ theme, hasMessages, statusLabel, statusColor, nodeCount, onNew, onHistory, onSettings, onResetLayout, onCollapse, onExport, exporting }: { theme: CanvasTheme; hasMessages: boolean; statusLabel: string; statusColor: string; nodeCount: number; onNew: () => void; onHistory: () => void; onSettings: () => void; onResetLayout: () => void; onCollapse: () => void; onExport: () => void; exporting: boolean }) {
|
||||
const appearance = useAppearanceStore((state) => state.appearance.canvas) || DEFAULT_CANVAS_APPEARANCE;
|
||||
return (
|
||||
<header data-agent-drag-handle className="agent-panel-header flex shrink-0 items-center gap-3">
|
||||
@@ -993,6 +994,7 @@ function AgentHeader({ theme, hasMessages, statusLabel, statusColor, nodeCount,
|
||||
<div className="agent-panel-context">{appearance.agentName} · 当前画布 {nodeCount} 个节点</div>
|
||||
</div>
|
||||
<div className="agent-header-actions flex items-center gap-0.5" style={{ color: theme.node.muted }}>
|
||||
<Button type="text" shape="circle" icon={<RotateCcw className="size-4" />} onClick={onResetLayout} aria-label="恢复 Agent 紧凑窗口" title="恢复默认窗口大小和位置" className="hidden sm:inline-flex" />
|
||||
<Button type="text" shape="circle" icon={<Download className="size-4" />} loading={exporting} onClick={onExport} aria-label="导出 Agent 调试记录" title="导出对话、工具参数、审批和错误(分享前请检查隐私)" />
|
||||
<Button type="text" shape="circle" icon={<MessageSquarePlus className="size-4" />} onClick={onNew} aria-label="新建对话" title="新建对话" />
|
||||
<Button type="text" shape="circle" icon={<History className="size-4" />} onClick={onHistory} aria-label="历史对话" title="历史对话" />
|
||||
|
||||
@@ -30,8 +30,8 @@
|
||||
}
|
||||
|
||||
.agent-panel-header {
|
||||
min-height: 80px;
|
||||
padding: var(--space-4) var(--space-5) var(--space-3) var(--space-6);
|
||||
min-height: 68px;
|
||||
padding: var(--space-3) var(--space-4) var(--space-2) var(--space-5);
|
||||
}
|
||||
|
||||
.agent-panel-title {
|
||||
@@ -55,8 +55,8 @@
|
||||
}
|
||||
|
||||
.agent-conversation {
|
||||
padding: var(--space-4) var(--space-6) var(--space-6);
|
||||
scroll-padding-block: var(--space-6);
|
||||
padding: var(--space-3) var(--space-5) var(--space-5);
|
||||
scroll-padding-block: var(--space-5);
|
||||
}
|
||||
|
||||
.agent-conversation-messages > * + * {
|
||||
@@ -193,11 +193,11 @@
|
||||
}
|
||||
|
||||
.agent-composer-wrap {
|
||||
padding: var(--space-2) var(--space-4) var(--space-4);
|
||||
padding: var(--space-2) var(--space-3) var(--space-3);
|
||||
}
|
||||
|
||||
.agent-composer-surface {
|
||||
padding: var(--space-4) var(--space-4) var(--space-3);
|
||||
padding: var(--space-3) var(--space-3) var(--space-2-half);
|
||||
border-radius: var(--agent-radius-surface, var(--radius-xl));
|
||||
background: var(--agent-surface-raised, var(--background));
|
||||
}
|
||||
@@ -216,27 +216,84 @@
|
||||
background: var(--agent-surface-raised, var(--background));
|
||||
}
|
||||
|
||||
/* 场景起步胶囊:选择作用于会话,未安装的技能会加入用户技能库。 */
|
||||
.agent-scene-capsules {
|
||||
border: 0;
|
||||
background: var(--agent-surface-soft, color-mix(in srgb, var(--foreground) 4%, transparent));
|
||||
/* 场景起步标签:透明底、彩色细描边,不叠加卡片表面。 */
|
||||
.agent-scene-capsules-heading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
padding-inline: var(--space-1);
|
||||
color: var(--agent-ink);
|
||||
font-size: var(--fs-caption);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.agent-scene-capsules-heading svg {
|
||||
width: var(--space-4);
|
||||
height: var(--space-4);
|
||||
color: var(--palette-node-skill);
|
||||
}
|
||||
|
||||
.agent-scene-capsule {
|
||||
border: 1px solid color-mix(in srgb, var(--foreground) 10%, transparent);
|
||||
background: var(--agent-surface-raised, var(--background));
|
||||
--scene-color: var(--palette-node-config);
|
||||
display: inline-flex;
|
||||
min-height: 36px;
|
||||
max-width: 100%;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
padding: var(--space-1-half) var(--space-3);
|
||||
border: 1px solid color-mix(in srgb, var(--scene-color) 58%, var(--agent-ink));
|
||||
border-radius: var(--r-full);
|
||||
background: transparent;
|
||||
color: color-mix(in srgb, var(--scene-color) 45%, var(--agent-ink));
|
||||
font-size: var(--fs-caption);
|
||||
line-height: 1.2;
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
transition: border-color 140ms ease, background 140ms ease, transform 140ms ease;
|
||||
transition: border-color 160ms ease, color 160ms ease, opacity 160ms ease;
|
||||
}
|
||||
|
||||
.agent-scene-capsule[data-scene="frequent"] { --scene-color: var(--palette-node-skill); }
|
||||
.agent-scene-capsule[data-scene="drama"] { --scene-color: var(--palette-node-script); }
|
||||
.agent-scene-capsule[data-scene="ecommerce"] { --scene-color: var(--palette-node-image); }
|
||||
.agent-scene-capsule[data-scene="creative"] { --scene-color: var(--palette-node-frame); }
|
||||
.agent-scene-capsule[data-scene="social"] { --scene-color: var(--palette-node-audio); }
|
||||
.agent-scene-capsule[data-scene="back"] { --scene-color: var(--agent-ink); }
|
||||
|
||||
.agent-scene-capsule svg {
|
||||
width: var(--space-4);
|
||||
height: var(--space-4);
|
||||
flex: 0 0 auto;
|
||||
stroke-width: 1.8;
|
||||
color: color-mix(in srgb, var(--scene-color) 75%, var(--agent-ink));
|
||||
}
|
||||
|
||||
.agent-scene-capsule-label {
|
||||
min-width: 0;
|
||||
max-width: 10rem;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.agent-scene-capsule-meta {
|
||||
flex: 0 0 auto;
|
||||
color: var(--agent-caption);
|
||||
font-size: var(--fs-label);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.agent-scene-capsule:hover:not(:disabled) {
|
||||
border-color: color-mix(in srgb, var(--scene-color) 82%, var(--agent-ink));
|
||||
color: color-mix(in srgb, var(--scene-color) 70%, var(--agent-ink));
|
||||
}
|
||||
|
||||
.agent-scene-capsule:hover:not(:disabled),
|
||||
.agent-scene-capsule:focus-visible {
|
||||
border-color: color-mix(in srgb, var(--agent-accent, var(--foreground)) 45%, transparent);
|
||||
background: var(--agent-surface-soft, color-mix(in srgb, var(--foreground) 6%, transparent));
|
||||
outline: 2px solid var(--agent-focus-ring);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.agent-scene-capsule:active:not(:disabled) {
|
||||
transform: translateY(1px);
|
||||
opacity: 0.65;
|
||||
}
|
||||
|
||||
.agent-scene-capsule:disabled {
|
||||
@@ -244,6 +301,14 @@
|
||||
opacity: 0.55;
|
||||
}
|
||||
|
||||
.agent-scene-capsules-note {
|
||||
margin: 0;
|
||||
padding-inline: var(--space-1);
|
||||
color: var(--agent-caption);
|
||||
font-size: var(--fs-label);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.agent-scene-capsules-scroll {
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
@@ -261,9 +326,6 @@
|
||||
.agent-scene-capsule {
|
||||
transition: none;
|
||||
}
|
||||
.agent-scene-capsule:active:not(:disabled) {
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
|
||||
@container (max-width: 400px) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useEffect, useRef, useState, type PointerEvent, type KeyboardEvent } from "react";
|
||||
import { AGENT_PANEL_LAYOUT_KEY, changeAgentPanelLayout, clampAgentPanelLayout, restoreAgentPanelLayout, type AgentPanelGesture, type AgentPanelLayout } from "@/lib/canvas/agent-panel-layout";
|
||||
import { AGENT_PANEL_LAYOUT_KEY, changeAgentPanelLayout, clampAgentPanelLayout, defaultAgentPanelLayout, restoreAgentPanelLayout, type AgentPanelGesture, type AgentPanelLayout } from "@/lib/canvas/agent-panel-layout";
|
||||
|
||||
const viewport = () => ({ width: window.innerWidth, height: window.innerHeight });
|
||||
|
||||
@@ -69,6 +69,7 @@ export function useAgentPanelLayout() {
|
||||
|
||||
return {
|
||||
style: compact ? { left: 0, top: 8, width: "100%", height: "calc(100dvh - 8px)" } : layout,
|
||||
reset: () => setLayout(defaultAgentPanelLayout(viewport())),
|
||||
onResizeKeyDown,
|
||||
pointerHandlers: { onPointerDown, onPointerMove, onPointerUp, onPointerCancel: onPointerUp, onLostPointerCapture: () => { gestureRef.current = null; } },
|
||||
};
|
||||
|
||||
@@ -58,10 +58,9 @@ type DockMetrics = {
|
||||
distance: number;
|
||||
};
|
||||
|
||||
// magnification 收敛:base 略增以适配圆角方块,magnified 幅度从 +14 降到 +8,
|
||||
// 避免 dock 整体跳动,同时保留 Aceternity 接近放大身份。
|
||||
// 桌面 dock 收紧常态与悬浮尺寸;触屏尺寸单独保留以保证点击目标。
|
||||
const DOCK_METRICS: Record<NonNullable<FloatingDockProps["size"]>, DockMetrics> = {
|
||||
default: { base: 30, magnified: 38, icon: 15, iconMagnified: 18, distance: 100 },
|
||||
default: { base: 28, magnified: 34, icon: 14, iconMagnified: 16, distance: 100 },
|
||||
compact: { base: 26, magnified: 32, icon: 13, iconMagnified: 16, distance: 84 },
|
||||
};
|
||||
|
||||
@@ -136,7 +135,7 @@ export const FloatingDock = forwardRef<HTMLDivElement, FloatingDockProps>(functi
|
||||
: "h-9 gap-0.5 px-0.5 pb-0.5"
|
||||
: size === "compact"
|
||||
? "h-8 gap-0.5 rounded-[var(--r-lg)] px-1 pb-1"
|
||||
: "h-10 gap-0.5 rounded-[var(--dock-radius)] px-1.5 pb-1",
|
||||
: "h-9 gap-0.5 rounded-[var(--dock-radius)] px-1.5 pb-0.5",
|
||||
className,
|
||||
)}
|
||||
style={style}
|
||||
|
||||
@@ -20,7 +20,7 @@ export function clampAgentPanelLayout(layout: AgentPanelLayout, viewport: AgentP
|
||||
}
|
||||
|
||||
export function restoreAgentPanelLayout(raw: string | null, viewport: AgentPanelViewport): AgentPanelLayout {
|
||||
const fallback = { width: 448, height: 720, left: viewport.width - 464, top: viewport.height - 732 };
|
||||
const fallback = defaultAgentPanelLayout(viewport);
|
||||
if (!raw) return clampAgentPanelLayout(fallback, viewport);
|
||||
try {
|
||||
const parsed: unknown = JSON.parse(raw);
|
||||
@@ -33,6 +33,12 @@ export function restoreAgentPanelLayout(raw: string | null, viewport: AgentPanel
|
||||
return clampAgentPanelLayout(fallback, viewport);
|
||||
}
|
||||
|
||||
export function defaultAgentPanelLayout(viewport: AgentPanelViewport): AgentPanelLayout {
|
||||
const width = 420;
|
||||
const height = 640;
|
||||
return clampAgentPanelLayout({ width, height, left: viewport.width - width - MARGIN, top: viewport.height - height - MARGIN }, viewport);
|
||||
}
|
||||
|
||||
export function changeAgentPanelLayout(start: AgentPanelLayout, gesture: AgentPanelGesture, dx: number, dy: number, viewport: AgentPanelViewport): AgentPanelLayout {
|
||||
if (gesture === "move") return clampAgentPanelLayout({ ...start, left: start.left + dx, top: start.top + dy }, viewport);
|
||||
const right = start.left + start.width;
|
||||
|
||||
@@ -249,7 +249,7 @@
|
||||
--canvas-inset-x: 16px; /* 浮层水平安全区(dock inset、顶栏 px)*/
|
||||
--canvas-inset-y: 16px; /* 浮层垂直安全区(dock 底部)*/
|
||||
--canvas-topbar-h: 64px; /* 顶部栏高度 */
|
||||
--canvas-topbar-offset: 72px; /* 顶栏下方浮层顶部偏移(topbar-h + 8px 间距)*/
|
||||
--canvas-topbar-offset: calc(var(--canvas-topbar-h) + 8px); /* 顶栏下方浮层顶部偏移 */
|
||||
--canvas-dock-popover-offset: 50px; /* dock 弹出面板底部偏移 */
|
||||
|
||||
/* --- 7. 动效(对应 #99 动效 token)--- */
|
||||
@@ -1691,9 +1691,9 @@
|
||||
--workspace-accent-soft: rgba(17, 17, 17, 0.06);
|
||||
--workspace-nav-active-bg: rgba(17, 17, 17, 0.075);
|
||||
--workspace-nav-active-hover-bg: rgba(17, 17, 17, 0.11);
|
||||
--workspace-sidebar-width: 312px;
|
||||
--workspace-sidebar-nav-width: 312px;
|
||||
--workspace-sidebar-collapsed-width: 86px;
|
||||
--workspace-sidebar-width: 200px;
|
||||
--workspace-sidebar-nav-width: 200px;
|
||||
--workspace-sidebar-collapsed-width: 64px;
|
||||
--workspace-shell-gutter: 0px;
|
||||
--workspace-card-gap: 0px;
|
||||
--workspace-topbar-height: 56px;
|
||||
@@ -1719,10 +1719,10 @@
|
||||
|
||||
/* 画布编辑器保持独立的全屏工作区,但沿用用户端的安全区和控件密度。 */
|
||||
.app-user-workspace.app-spatial-workspace {
|
||||
--canvas-topbar-h: 64px;
|
||||
--canvas-inset-x: 16px;
|
||||
--canvas-inset-y: 16px;
|
||||
--canvas-dock-popover-offset: 50px;
|
||||
--canvas-topbar-h: 56px;
|
||||
--canvas-inset-x: 12px;
|
||||
--canvas-inset-y: 12px;
|
||||
--canvas-dock-popover-offset: 46px;
|
||||
}
|
||||
|
||||
/* 画布编辑器和管理后台不继承用户端的圆角卡片外壳。 */
|
||||
@@ -15180,12 +15180,12 @@ html:not(.dark) .creation-home .creation-user-message-copy-wrap { box-shadow: 0
|
||||
|
||||
/* ===== 2026-09 user workspace alignment =====
|
||||
这一组规则只作用于真实用户端壳层和首页,后台与画布编辑器保持原有职责和交互。
|
||||
几何基准:侧栏 216px、顶部 64px、导航行 44px、首页内容最大宽度 1180px。 */
|
||||
几何基准:侧栏 200px、顶部 56px、导航行 40px、首页内容最大宽度 1180px。 */
|
||||
.app-user-workspace {
|
||||
--workspace-reference-sidebar: 216px;
|
||||
--workspace-reference-collapsed: 70px;
|
||||
--workspace-reference-topbar: 64px;
|
||||
--workspace-reference-row: 44px;
|
||||
--workspace-reference-sidebar: 200px;
|
||||
--workspace-reference-collapsed: 64px;
|
||||
--workspace-reference-topbar: 56px;
|
||||
--workspace-reference-row: 40px;
|
||||
--workspace-reference-page-bg: #ffffff;
|
||||
background: var(--workspace-reference-page-bg);
|
||||
}
|
||||
@@ -17844,8 +17844,8 @@ html:not(.dark) .creation-home .creation-user-message-copy-wrap { box-shadow: 0
|
||||
.app-user-workspace .app-workspace-sidebar-scroll-area { padding: 22px 12px 12px !important; gap: 24px !important; }
|
||||
.app-user-workspace .app-workspace-sidebar-footer { padding: 12px 12px 14px !important; border-top: 0 !important; }
|
||||
.app-user-workspace .app-workspace-nav-link {
|
||||
min-height: 44px !important;
|
||||
padding: 4px 10px !important;
|
||||
min-height: var(--workspace-reference-row) !important;
|
||||
padding: 3px 10px !important;
|
||||
border-radius: 13px !important;
|
||||
color: var(--user-ink-muted) !important;
|
||||
font-size: 13px !important;
|
||||
@@ -17860,13 +17860,13 @@ html:not(.dark) .creation-home .creation-user-message-copy-wrap { box-shadow: 0
|
||||
.app-user-workspace .app-workspace-nav-link:focus-visible:not(.is-active) { background: var(--user-surface-hover) !important; color: var(--user-ink) !important; }
|
||||
.app-user-workspace .app-workspace-nav-group-toggle { color: var(--user-ink-soft) !important; }
|
||||
.app-user-workspace .app-workspace-sidebar.is-collapsed .app-workspace-sidebar-scroll-area { padding-inline: 8px !important; }
|
||||
.app-user-workspace .app-workspace-sidebar.is-collapsed .app-workspace-nav-link { width: 54px !important; }
|
||||
.app-user-workspace .app-workspace-sidebar.is-collapsed .app-workspace-nav-link { width: 48px !important; }
|
||||
|
||||
.app-user-workspace .app-workspace-topbar {
|
||||
min-height: 64px !important;
|
||||
height: 64px !important;
|
||||
min-height: var(--workspace-reference-topbar) !important;
|
||||
height: var(--workspace-reference-topbar) !important;
|
||||
overflow: visible;
|
||||
padding-inline: 24px !important;
|
||||
padding-inline: 20px !important;
|
||||
border-bottom: 0 !important;
|
||||
background: color-mix(in srgb, var(--user-page-bg) 92%, transparent) !important;
|
||||
color: var(--user-ink) !important;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, it } from "bun:test";
|
||||
import { changeAgentPanelLayout, clampAgentPanelLayout, restoreAgentPanelLayout } from "@/lib/canvas/agent-panel-layout";
|
||||
import { changeAgentPanelLayout, clampAgentPanelLayout, defaultAgentPanelLayout, restoreAgentPanelLayout } from "@/lib/canvas/agent-panel-layout";
|
||||
import { agentErrorPresentation, agentSubmissionErrorTitle } from "@/lib/canvas/agent-error-presentation";
|
||||
import { ApiError } from "@/services/api/request";
|
||||
|
||||
@@ -15,6 +15,10 @@ describe("Agent window layout", () => {
|
||||
expect(layout.height).toBe(626);
|
||||
expect(layout.top).toBe(12);
|
||||
});
|
||||
it("offers a compact reset without altering a saved window until requested", () => {
|
||||
expect(restoreAgentPanelLayout(JSON.stringify(start), viewport)).toEqual(start);
|
||||
expect(defaultAgentPanelLayout(viewport)).toEqual({ left: 848, top: 148, width: 420, height: 640 });
|
||||
});
|
||||
it("keeps the bottom/right anchor when resizing from top/left", () => {
|
||||
const layout = changeAgentPanelLayout(start, "northwest", -120, 60, viewport);
|
||||
expect(layout).toEqual({ left: 696, top: 128, width: 568, height: 660 });
|
||||
|
||||
Reference in New Issue
Block a user