fix(canvas): 文件夹主题 - 对齐参考材质并分离内容预览

This commit is contained in:
echoD886
2026-08-20 15:59:51 +08:00
parent d8637f5032
commit b2923b1c08
5 changed files with 325 additions and 78 deletions

View File

@@ -1,7 +1,10 @@
.folder-preview-lab {
min-height: 100dvh;
overflow: auto;
background: var(--workspace-canvas);
background-color: var(--workspace-canvas);
background-image: radial-gradient(circle, var(--border-semantic) 1.15px, transparent 1.35px);
background-position: center top;
background-size: 24px 24px;
color: var(--fg);
padding: var(--space-10) var(--space-12) var(--space-20);
}
@@ -49,6 +52,43 @@
font-size: var(--fs-caption);
}
.folder-preview-lab-controls {
display: flex;
flex: 0 0 auto;
align-items: center;
gap: var(--space-3);
}
.folder-preview-lab-theme {
display: inline-flex;
border-radius: var(--r-full);
background: var(--library-surface);
padding: var(--space-1);
}
.folder-preview-lab-theme button {
display: inline-flex;
min-height: 40px;
align-items: center;
gap: var(--space-2);
border: 0;
border-radius: var(--r-full);
background: transparent;
padding: var(--space-2) var(--space-3);
color: var(--muted-foreground);
font-size: var(--fs-caption);
}
.folder-preview-lab-theme button[aria-pressed="true"] {
background: var(--control-selected-bg);
color: var(--control-selected-fg);
}
.folder-preview-lab-theme svg {
width: var(--space-4);
height: var(--space-4);
}
.folder-preview-lab-guide svg,
.folder-preview-lab-open svg,
.folder-preview-lab-drawer-header button svg {
@@ -56,10 +96,38 @@
height: var(--space-4);
}
.folder-preview-lab-grid {
display: grid;
.folder-preview-lab-section {
max-width: 1280px;
margin: 0 auto;
}
.folder-preview-lab-section + .folder-preview-lab-section {
margin-top: var(--space-16);
}
.folder-preview-lab-section-heading {
display: flex;
margin-bottom: var(--space-6);
align-items: end;
justify-content: space-between;
gap: var(--space-6);
}
.folder-preview-lab-section-heading h2 {
margin: 0;
font-size: var(--fs-title);
font-weight: 620;
letter-spacing: -.02em;
}
.folder-preview-lab-section-heading p {
margin: 0;
color: var(--muted-foreground);
font-size: var(--fs-caption);
}
.folder-preview-lab-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(360px, 1fr));
gap: var(--space-10) var(--space-8);
}
@@ -214,7 +282,9 @@
@media (max-width: 760px) {
.folder-preview-lab { padding: var(--space-6) var(--space-4) var(--space-12); }
.folder-preview-lab-header { align-items: flex-start; flex-direction: column; }
.folder-preview-lab-header,
.folder-preview-lab-controls,
.folder-preview-lab-section-heading { align-items: flex-start; flex-direction: column; }
.folder-preview-lab-grid { grid-template-columns: minmax(0, 1fr); }
.folder-preview-lab-folder { transform: scale(.86); transform-origin: top center; margin-bottom: -40px; }
.folder-preview-lab-drawer { inset: var(--space-2); padding: var(--space-4); }

View File

@@ -1,7 +1,8 @@
import { useMemo, useState } from "react";
import { ChevronLeft, FolderOpen, ImagePlus, MousePointer2 } from "lucide-react";
import { ChevronLeft, FolderOpen, ImagePlus, Moon, MousePointer2, Sun } from "lucide-react";
import { CanvasFolderPreview } from "@/components/canvas/canvas-folder-preview";
import { useThemeStore } from "@/stores/use-theme-store";
import type { CanvasFolderStyle, CanvasNodeData } from "@/types/canvas";
import { CanvasNodeType } from "@/types/canvas";
@@ -16,6 +17,23 @@ const STYLE_PRESETS: Array<{ style: CanvasFolderStyle; name: string; hint: strin
{ style: "compact", name: "紧凑资料", hint: "衍生 · 高频调用" },
];
const PRESET_GROUPS = [
{
key: "reference",
title: "原图复刻",
description: "三种参考结构:玻璃加号、内容陈列、午夜封面。",
presets: STYLE_PRESETS.slice(0, 3),
},
{
key: "derived",
title: "风格衍生",
description: "沿用同一主题皮肤与交互合同的工作流变体。",
presets: STYLE_PRESETS.slice(3),
},
];
const FOLDER_THEME_COVER = "/images/canvas/folder-default-cover.png";
const SAMPLE_MEDIA = [
"/short-drama-styles/fantasy-3d.jpg",
"/short-drama-styles/suspense-noir.jpg",
@@ -40,12 +58,12 @@ function makeFolderNode(style: CanvasFolderStyle, index: number): CanvasNodeData
return {
id: `folder-${style}`,
type: CanvasNodeType.Frame,
title: ["我的素材", "项目资料", "视觉档案", "灵感收藏", "影像资产", "快速调用"][index],
title: ["My Files", "Archives", "My Files", "灵感收藏", "影像资产", "快速调用"][index],
position: { x: 0, y: 0 },
width: 360,
height: 280,
metadata: {
folder: { style, createdAt: new Date(2026, 7, 19 + index).toISOString() },
folder: { style, createdAt: new Date(2026, 7, 19 + index).toISOString(), themeCover: FOLDER_THEME_COVER },
frame: { collapsed: true, expandedWidth: 760, expandedHeight: 520 },
},
};
@@ -63,9 +81,11 @@ const INITIAL_CHILDREN = Object.fromEntries(
) as Record<string, CanvasNodeData[]>;
export default function FolderPreviewLab() {
const theme = useThemeStore((state) => state.theme);
const setTheme = useThemeStore((state) => state.setTheme);
const [folders, setFolders] = useState(INITIAL_FOLDERS);
const [childrenByFolder, setChildrenByFolder] = useState(INITIAL_CHILDREN);
const [selectedId, setSelectedId] = useState(INITIAL_FOLDERS[0].id);
const [selectedId, setSelectedId] = useState<string | null>(null);
const [openedId, setOpenedId] = useState<string | null>(null);
const openedFolder = useMemo(() => folders.find((folder) => folder.id === openedId), [folders, openedId]);
const openedChildren = openedId ? childrenByFolder[openedId] || [] : [];
@@ -95,64 +115,82 @@ export default function FolderPreviewLab() {
<div>
<span className="folder-preview-lab-kicker">DEV · INTERACTIVE FOLDER LAB</span>
<h1>通用素材文件夹</h1>
<p>悬停查看动效,单击选中,双击打开;标题和右上角菜单都可以直接操作。</p>
<p>红蓝抽象图只作为文件夹主题皮肤;打开文件夹后,内部素材以独立内容卡片展示。悬停、单击和双击都有对应反馈。</p>
</div>
<div className="folder-preview-lab-guide">
<MousePointer2 aria-hidden />
<span>试试 hover、双击与样式切换</span>
<div className="folder-preview-lab-controls">
<div className="folder-preview-lab-theme" aria-label="预览主题">
<button type="button" aria-pressed={theme === "light"} onClick={() => setTheme("light")}>
<Sun aria-hidden /> 浅色
</button>
<button type="button" aria-pressed={theme === "dark"} onClick={() => setTheme("dark")}>
<Moon aria-hidden /> 深色
</button>
</div>
<div className="folder-preview-lab-guide">
<MousePointer2 aria-hidden />
<span>试试 hover、双击与样式切换</span>
</div>
</div>
</header>
<section className="folder-preview-lab-grid" aria-label="文件夹样式预览">
{folders.map((folder, index) => {
const preset = STYLE_PRESETS[index];
const childNodes = childrenByFolder[folder.id] || [];
return (
<article key={folder.id} className="folder-preview-lab-item">
<div className="folder-preview-lab-item-copy">
<strong>{preset.name}</strong>
<span>{preset.hint}</span>
</div>
<div
className="folder-preview-lab-folder"
role="button"
tabIndex={0}
aria-label={`打开 ${folder.title}`}
onClick={() => setSelectedId(folder.id)}
onDoubleClick={() => setOpenedId(folder.id)}
onKeyDown={(event) => {
if (event.key === "Enter" || event.key === " ") {
event.preventDefault();
setOpenedId(folder.id);
}
}}
>
<CanvasFolderPreview
data={folder}
childNodes={childNodes}
active={selectedId === folder.id}
isDropTarget={false}
readOnly={false}
onToggleCollapsed={setOpenedId}
onTitleChange={(nodeId, title) => updateFolder(nodeId, (current) => ({ ...current, title }))}
onStyleChange={(nodeId, style) =>
updateFolder(nodeId, (current) => ({
...current,
metadata: {
...current.metadata,
folder: { ...current.metadata!.folder!, style },
},
}))
}
/>
</div>
<button type="button" className="folder-preview-lab-open" onClick={() => setOpenedId(folder.id)}>
<FolderOpen aria-hidden /> 打开文件夹
</button>
</article>
);
})}
</section>
{PRESET_GROUPS.map((group) => (
<section key={group.key} className="folder-preview-lab-section" aria-labelledby={`folder-preview-${group.key}`}>
<div className="folder-preview-lab-section-heading">
<h2 id={`folder-preview-${group.key}`}>{group.title}</h2>
<p>{group.description}</p>
</div>
<div className="folder-preview-lab-grid">
{group.presets.map((preset) => {
const folder = folders.find((item) => item.id === `folder-${preset.style}`)!;
const childNodes = childrenByFolder[folder.id] || [];
return (
<article key={folder.id} className="folder-preview-lab-item">
<div className="folder-preview-lab-item-copy">
<strong>{preset.name}</strong>
<span>{preset.hint}</span>
</div>
<div
className="folder-preview-lab-folder"
role="button"
tabIndex={0}
aria-label={`打开 ${folder.title}`}
onClick={() => setSelectedId(folder.id)}
onDoubleClick={() => setOpenedId(folder.id)}
onKeyDown={(event) => {
if (event.key === "Enter" || event.key === " ") {
event.preventDefault();
setOpenedId(folder.id);
}
}}
>
<CanvasFolderPreview
data={folder}
childNodes={childNodes}
active={selectedId === folder.id}
isDropTarget={false}
readOnly={false}
onToggleCollapsed={setOpenedId}
onTitleChange={(nodeId, title) => updateFolder(nodeId, (current) => ({ ...current, title }))}
onStyleChange={(nodeId, style) =>
updateFolder(nodeId, (current) => ({
...current,
metadata: {
...current.metadata,
folder: { ...current.metadata!.folder!, style },
},
}))
}
/>
</div>
<button type="button" className="folder-preview-lab-open" onClick={() => setOpenedId(folder.id)}>
<FolderOpen aria-hidden /> 打开文件夹
</button>
</article>
);
})}
</div>
</section>
))}
{openedFolder ? (
<section className="folder-preview-lab-drawer" aria-label={`${openedFolder.title} 内容`}>