diff --git a/web/src/components/canvas/canvas-folder-preview.tsx b/web/src/components/canvas/canvas-folder-preview.tsx index 681fb9f94..36cf6d6e5 100644 --- a/web/src/components/canvas/canvas-folder-preview.tsx +++ b/web/src/components/canvas/canvas-folder-preview.tsx @@ -39,7 +39,7 @@ export const CanvasFolderPreview = React.memo(function CanvasFolderPreview({ const [editing, setEditing] = useState(false); const [title, setTitle] = useState(data.title); const previewNodes = useMemo(() => childNodes.filter(canPreviewFolderNode).slice(0, 3), [childNodes]); - const primaryNode = previewNodes[0]; + const themeCover = data.metadata?.folder?.themeCover || DEFAULT_FOLDER_COVER; const showAdd = style === "glass" || childNodes.length === 0; useEffect(() => setTitle(data.title), [data.title]); @@ -54,20 +54,22 @@ export const CanvasFolderPreview = React.memo(function CanvasFolderPreview({ return (
- +
+
+
{(previewNodes.length ? previewNodes : [undefined, undefined]).map((node, index) => (
- +
))}
- +
@@ -180,7 +182,11 @@ function canPreviewFolderNode(node: CanvasNodeData) { return node.type !== CanvasNodeType.Frame; } -function FolderNodeMedia({ node, fallback = false }: { node?: CanvasNodeData; fallback?: boolean }) { +function FolderThemeMedia({ source }: { source: string }) { + return ; +} + +function FolderNodeMedia({ node }: { node?: CanvasNodeData }) { if (node?.type === CanvasNodeType.Image && node.metadata?.content) { return ; } @@ -198,7 +204,7 @@ function FolderNodeMedia({ node, fallback = false }: { node?: CanvasNodeData; fa return {node.metadata?.content || node.title}; } if (node) return ; - return fallback ? : ; + return ; } function formatFolderDate(value?: string) { diff --git a/web/src/pages/dev/folder-preview-lab.css b/web/src/pages/dev/folder-preview-lab.css index ce78dad46..c567d1395 100644 --- a/web/src/pages/dev/folder-preview-lab.css +++ b/web/src/pages/dev/folder-preview-lab.css @@ -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); } diff --git a/web/src/pages/dev/folder-preview-lab.tsx b/web/src/pages/dev/folder-preview-lab.tsx index 08c388e59..2a25ec90b 100644 --- a/web/src/pages/dev/folder-preview-lab.tsx +++ b/web/src/pages/dev/folder-preview-lab.tsx @@ -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; 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(null); const [openedId, setOpenedId] = useState(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() {
DEV · INTERACTIVE FOLDER LAB

通用素材文件夹

-

悬停查看动效,单击选中,双击打开;标题和右上角菜单都可以直接操作。

+

红蓝抽象图只作为文件夹主题皮肤;打开文件夹后,内部素材以独立内容卡片展示。悬停、单击和双击都有对应反馈。

-
- - 试试 hover、双击与样式切换 +
+
+ + +
+
+ + 试试 hover、双击与样式切换 +
-
- {folders.map((folder, index) => { - const preset = STYLE_PRESETS[index]; - const childNodes = childrenByFolder[folder.id] || []; - return ( -
-
- {preset.name} - {preset.hint} -
-
setSelectedId(folder.id)} - onDoubleClick={() => setOpenedId(folder.id)} - onKeyDown={(event) => { - if (event.key === "Enter" || event.key === " ") { - event.preventDefault(); - setOpenedId(folder.id); - } - }} - > - updateFolder(nodeId, (current) => ({ ...current, title }))} - onStyleChange={(nodeId, style) => - updateFolder(nodeId, (current) => ({ - ...current, - metadata: { - ...current.metadata, - folder: { ...current.metadata!.folder!, style }, - }, - })) - } - /> -
- -
- ); - })} -
+ {PRESET_GROUPS.map((group) => ( +
+
+

{group.title}

+

{group.description}

+
+
+ {group.presets.map((preset) => { + const folder = folders.find((item) => item.id === `folder-${preset.style}`)!; + const childNodes = childrenByFolder[folder.id] || []; + return ( +
+
+ {preset.name} + {preset.hint} +
+
setSelectedId(folder.id)} + onDoubleClick={() => setOpenedId(folder.id)} + onKeyDown={(event) => { + if (event.key === "Enter" || event.key === " ") { + event.preventDefault(); + setOpenedId(folder.id); + } + }} + > + updateFolder(nodeId, (current) => ({ ...current, title }))} + onStyleChange={(nodeId, style) => + updateFolder(nodeId, (current) => ({ + ...current, + metadata: { + ...current.metadata, + folder: { ...current.metadata!.folder!, style }, + }, + })) + } + /> +
+ +
+ ); + })} +
+
+ ))} {openedFolder ? (
diff --git a/web/src/styles/globals.css b/web/src/styles/globals.css index e4be91974..973769d51 100644 --- a/web/src/styles/globals.css +++ b/web/src/styles/globals.css @@ -6021,6 +6021,7 @@ } .canvas-folder-preview { + --canvas-folder-front-shape: path("M 28 58 H 112 C 126 58 135 62 144 70 L 174 96 C 182 104 191 108 204 108 H 332 C 348 108 360 120 360 136 V 252 C 360 268 348 280 332 280 H 28 C 12 280 0 268 0 252 V 86 C 0 70 12 58 28 58 Z"); position: relative; width: 100%; height: 100%; @@ -6062,11 +6063,14 @@ .canvas-folder-back, .canvas-folder-front, - .canvas-folder-preview-card { + .canvas-folder-preview-card, + .canvas-folder-paper-sheet { position: absolute; overflow: hidden; } + .canvas-folder-paper-sheet { display: none; } + .canvas-folder-back { z-index: 1; inset: 0; @@ -6125,7 +6129,7 @@ inset: 0; border-radius: var(--r-5xl); box-shadow: inset 0 0 0 2px var(--canvas-folder-edge); - clip-path: path("M 28 58 H 112 C 126 58 135 62 144 70 L 174 96 C 182 104 191 108 204 108 H 332 C 348 108 360 120 360 136 V 252 C 360 268 348 280 332 280 H 28 C 12 280 0 268 0 252 V 86 C 0 70 12 58 28 58 Z"); + clip-path: var(--canvas-folder-front-shape); backdrop-filter: blur(24px) saturate(118%); transition: transform var(--motion-state) var(--motion-ease-out); } @@ -6232,7 +6236,7 @@ box-shadow: inset 0 0 0 2px color-mix(in srgb, var(--canvas-folder-action-fg) 22%, transparent), 0 12px 26px color-mix(in srgb, var(--cn-spatial-shadow) 52%, transparent); } - .canvas-folder-add > svg { width: 34px; height: 34px; stroke-width: 1.5; } + .canvas-folder-add > svg { width: 34px; height: 34px; color: var(--canvas-folder-action-fg); stroke: currentColor; stroke-width: 1.5; } .canvas-folder-options { width: 66px; @@ -6292,30 +6296,158 @@ .canvas-folder-file-icon { position: absolute; inset: 0; width: 36px; height: 36px; margin: auto; opacity: .42; } .canvas-folder-file-sheet { display: block; width: 100%; height: 100%; background: var(--canvas-folder-surface-strong); } - /* 三个参考态。stacked 露出内容卡;midnight 把标题压进深色封面标签。 */ + /* 三个参考态严格保留各自材质:玻璃磨砂、白纸层叠、红黑夜间封面。 */ + .canvas-folder-preview-glass { + --canvas-folder-front-shape: path("M 28 70 H 112 C 126 70 136 74 146 82 L 174 104 C 183 112 192 116 205 116 H 332 C 348 116 360 128 360 144 V 252 C 360 268 348 280 332 280 H 28 C 12 280 0 268 0 252 V 98 C 0 82 12 70 28 70 Z"); + --canvas-folder-surface: rgba(248, 249, 251, .62); + --canvas-folder-surface-strong: rgba(251, 252, 253, .9); + --canvas-folder-edge: rgba(255, 255, 255, .78); + --canvas-folder-ink: #17181b; + --canvas-folder-muted: rgba(23, 24, 27, .54); + --canvas-folder-action-bg: rgba(8, 9, 11, .94); + --canvas-folder-action-fg: #fff; + } + + .canvas-folder-preview-glass .canvas-folder-front-media { opacity: .66; } + .canvas-folder-preview-glass .canvas-folder-front-tint { background: rgba(249, 250, 252, .56); } + .canvas-folder-preview-glass .canvas-folder-front { + box-shadow: inset 0 0 0 2px rgba(255, 255, 255, .74), 0 18px 34px rgba(12, 18, 28, .12); + backdrop-filter: blur(28px) saturate(124%); + } + + .dark .canvas-folder-preview-glass { + --canvas-folder-surface: rgba(10, 12, 16, .54); + --canvas-folder-surface-strong: rgba(13, 15, 20, .82); + --canvas-folder-edge: rgba(255, 255, 255, .2); + --canvas-folder-ink: rgba(255, 255, 255, .96); + --canvas-folder-muted: rgba(255, 255, 255, .58); + --canvas-folder-action-bg: rgba(246, 247, 249, .94); + --canvas-folder-action-fg: #111318; + } + + .dark .canvas-folder-preview-glass .canvas-folder-front-media { opacity: .78; } + .dark .canvas-folder-preview-glass .canvas-folder-front-tint { background: rgba(7, 9, 13, .5); } + .dark .canvas-folder-preview-glass .canvas-folder-front { + box-shadow: inset 0 0 0 2px rgba(255, 255, 255, .18), 0 22px 42px rgba(0, 0, 0, .42); + } + .canvas-folder-preview-glass .canvas-folder-preview-stack, .canvas-folder-preview-midnight .canvas-folder-preview-stack, .canvas-folder-preview-cinema .canvas-folder-preview-stack { opacity: 0; } - .canvas-folder-preview-stacked .canvas-folder-back, + .canvas-folder-preview-stacked { + --canvas-folder-front-shape: path("M 28 116 H 100 C 116 116 126 120 138 130 L 164 150 C 173 158 183 162 196 162 H 332 C 348 162 360 174 360 190 V 252 C 360 268 348 280 332 280 H 28 C 12 280 0 268 0 252 V 144 C 0 128 12 116 28 116 Z"); + --canvas-folder-surface: rgba(248, 249, 250, .72); + --canvas-folder-surface-strong: rgba(255, 255, 255, .94); + --canvas-folder-edge: rgba(255, 255, 255, .84); + --canvas-folder-ink: #17181b; + --canvas-folder-muted: rgba(23, 24, 27, .5); + --canvas-folder-action-bg: rgba(235, 237, 240, .78); + --canvas-folder-action-fg: #16181b; + } + + .canvas-folder-preview-stacked .canvas-folder-back { opacity: 0; } + .canvas-folder-preview-stacked .canvas-folder-paper-sheet { + z-index: 3; + top: 4px; + left: 80px; + display: block; + width: 238px; + height: 214px; + border-radius: var(--r-4xl); + background: rgba(255, 255, 255, .96); + box-shadow: inset 0 0 0 1px rgba(255, 255, 255, .82), 0 18px 34px rgba(21, 24, 30, .12); + } + + .canvas-folder-preview-stacked .canvas-folder-preview-card-1 { + top: 52px; + left: 16px; + width: 178px; + height: 154px; + transform: rotate(-5deg); + } + + .canvas-folder-preview-stacked .canvas-folder-preview-card-2 { + top: 46px; + right: 8px; + width: 176px; + height: 154px; + transform: rotate(5deg); + } + + .canvas-folder-preview-stacked .canvas-folder-preview-card-3 { opacity: 0; } + .canvas-folder-preview-stacked .canvas-folder-front-media { opacity: .18; } + .canvas-folder-preview-stacked .canvas-folder-front-tint { background: rgba(232, 234, 238, .72); } + .canvas-folder-preview-stacked .canvas-folder-front { + box-shadow: inset 0 0 0 2px rgba(255, 255, 255, .82), 0 20px 38px rgba(21, 24, 30, .14); + backdrop-filter: blur(30px) saturate(112%); + } + + .dark .canvas-folder-preview-stacked { + --canvas-folder-surface: rgba(19, 21, 26, .7); + --canvas-folder-surface-strong: rgba(29, 32, 38, .94); + --canvas-folder-edge: rgba(255, 255, 255, .18); + --canvas-folder-ink: rgba(255, 255, 255, .96); + --canvas-folder-muted: rgba(255, 255, 255, .56); + --canvas-folder-action-bg: rgba(45, 48, 55, .82); + --canvas-folder-action-fg: rgba(255, 255, 255, .94); + } + + .dark .canvas-folder-preview-stacked .canvas-folder-paper-sheet { + background: rgba(38, 41, 48, .98); + box-shadow: inset 0 0 0 1px rgba(255, 255, 255, .1), 0 20px 42px rgba(0, 0, 0, .42); + } + + .dark .canvas-folder-preview-stacked .canvas-folder-front-tint { background: rgba(17, 19, 24, .72); } + .dark .canvas-folder-preview-stacked .canvas-folder-front { + box-shadow: inset 0 0 0 2px rgba(255, 255, 255, .14), 0 22px 42px rgba(0, 0, 0, .46); + } + .canvas-folder-preview-paper .canvas-folder-back, .canvas-folder-preview-compact .canvas-folder-back { background: var(--canvas-folder-surface-strong); } - .canvas-folder-preview-stacked .canvas-folder-front-media, .canvas-folder-preview-paper .canvas-folder-front-media, .canvas-folder-preview-compact .canvas-folder-front-media { opacity: .24; } - .canvas-folder-preview-stacked .canvas-folder-copy, + .canvas-folder-preview-stacked .canvas-folder-copy { top: 142px; bottom: auto; } .canvas-folder-preview-paper .canvas-folder-copy { top: 132px; bottom: auto; } .canvas-folder-preview-midnight, - .canvas-folder-preview-cinema { --canvas-folder-ink: rgba(255, 255, 255, .96); --canvas-folder-muted: rgba(255, 255, 255, .58); --canvas-folder-edge: rgba(255, 255, 255, .19); --canvas-folder-surface: rgba(4, 5, 7, .78); --canvas-folder-surface-strong: rgba(8, 9, 12, .9); } + .canvas-folder-preview-cinema { + --canvas-folder-ink: rgba(255, 255, 255, .96); + --canvas-folder-muted: rgba(255, 255, 255, .58); + --canvas-folder-edge: rgba(255, 255, 255, .19); + --canvas-folder-surface: rgba(4, 5, 8, .7); + --canvas-folder-surface-strong: rgba(7, 8, 12, .9); + --canvas-folder-action-bg: rgba(5, 6, 9, .88); + --canvas-folder-action-fg: rgba(255, 255, 255, .96); + } + .canvas-folder-preview-midnight { + --canvas-folder-front-shape: path("M 28 116 H 118 C 134 116 145 121 157 132 L 180 151 C 189 159 198 162 211 162 H 332 C 348 162 360 174 360 190 V 252 C 360 268 348 280 332 280 H 28 C 12 280 0 268 0 252 V 144 C 0 128 12 116 28 116 Z"); + } + .canvas-folder-preview-midnight .canvas-folder-back, + .canvas-folder-preview-cinema .canvas-folder-back { + inset: 0 0 34px; + border-radius: var(--r-5xl); + clip-path: none; + } .canvas-folder-preview-midnight .canvas-folder-copy, .canvas-folder-preview-cinema .canvas-folder-copy { top: 142px; right: 34px; bottom: auto; left: auto; max-width: 170px; } .canvas-folder-preview-midnight .canvas-folder-front-media, - .canvas-folder-preview-cinema .canvas-folder-front-media { opacity: .78; } + .canvas-folder-preview-cinema .canvas-folder-front-media { opacity: .72; } .canvas-folder-preview-midnight .canvas-folder-front-tint, - .canvas-folder-preview-cinema .canvas-folder-front-tint { background: color-mix(in srgb, var(--canvas-folder-surface) 88%, transparent); } + .canvas-folder-preview-cinema .canvas-folder-front-tint { background: rgba(4, 5, 8, .7); } + .canvas-folder-preview-midnight .canvas-folder-front, + .canvas-folder-preview-cinema .canvas-folder-front { + box-shadow: inset 0 0 0 2px rgba(255, 255, 255, .15), 0 24px 48px rgba(0, 0, 0, .44); + backdrop-filter: blur(30px) saturate(118%); + } + + .dark .canvas-folder-preview-midnight .canvas-folder-front-tint { background: rgba(2, 3, 6, .78); } + .dark .canvas-folder-preview-midnight .canvas-folder-front-media { opacity: .82; } + + .canvas-folder-preview-glass .canvas-folder-meta, + .canvas-folder-preview-stacked .canvas-folder-meta { display: none; } .canvas-folder-preview-paper { --canvas-folder-surface: color-mix(in srgb, var(--background) 88%, transparent); } .canvas-folder-preview-cinema { filter: drop-shadow(0 28px 32px color-mix(in srgb, var(--cn-spatial-shadow) 72%, transparent)); } diff --git a/web/src/types/canvas.ts b/web/src/types/canvas.ts index 37ccda9a6..14ed7f85f 100644 --- a/web/src/types/canvas.ts +++ b/web/src/types/canvas.ts @@ -287,6 +287,7 @@ export type CanvasNodeMetadata = { folder?: { style: CanvasFolderStyle; createdAt: string; + themeCover?: string; assetFolderId?: string; projectId?: string; };