feat(assets): 文件夹主题 - 新增多主题皮肤并同步主线

This commit is contained in:
echoD886
2026-08-20 16:33:51 +08:00
93 changed files with 4081 additions and 1143 deletions

View File

@@ -0,0 +1,22 @@
import { useEffect, useState, type ReactNode } from "react";
import { ChevronDown } from "lucide-react";
export type GenerationToolStatus = "running" | "completed" | "error" | "cancelled";
export function GenerationToolCard({ status, isBulk = false, heading, children }: { status: GenerationToolStatus; isBulk?: boolean; heading: ReactNode; children: ReactNode }) {
const [open, setOpen] = useState(status !== "completed" || !isBulk);
useEffect(() => {
if (status !== "completed") setOpen(true);
}, [status]);
return <>
<div className="creation-message-heading">
{heading}
<button type="button" className="ml-auto grid size-7 shrink-0 place-items-center rounded-md text-foreground/45 transition-colors hover:bg-surface-hover hover:text-foreground" aria-label={open ? "收起生成详情" : "展开生成详情"} aria-expanded={open} onClick={() => setOpen((value) => !value)}>
<ChevronDown className={`size-4 transition-transform${open ? " rotate-180" : ""}`} />
</button>
</div>
{open ? children : null}
</>;
}

View File

@@ -0,0 +1,41 @@
import { useEffect, useRef, useState } from "react";
import { ChevronDown, Sparkles } from "lucide-react";
import { AIMessageMarkdown } from "@/components/ai/ai-message-markdown";
export function MessageReasoning({ reasoning, isStreaming }: { reasoning: string; isStreaming: boolean }) {
const [open, setOpen] = useState(isStreaming);
const [durationSeconds, setDurationSeconds] = useState<number>();
const startedAtRef = useRef<number | undefined>(undefined);
const autoClosedRef = useRef(false);
useEffect(() => {
if (!isStreaming) return;
startedAtRef.current ??= Date.now();
setOpen(true);
}, [isStreaming]);
useEffect(() => {
if (isStreaming || !startedAtRef.current) return;
setDurationSeconds(Math.max(1, Math.ceil((Date.now() - startedAtRef.current) / 1000)));
startedAtRef.current = undefined;
}, [isStreaming]);
useEffect(() => {
if (isStreaming || !open || autoClosedRef.current || durationSeconds === undefined) return;
const timer = window.setTimeout(() => {
setOpen(false);
autoClosedRef.current = true;
}, 1000);
return () => window.clearTimeout(timer);
}, [durationSeconds, isStreaming, open]);
return <div className="mb-2 text-xs text-foreground/55">
<button type="button" className="inline-flex min-h-8 items-center gap-1.5 rounded-md px-2 transition-colors hover:bg-surface-hover hover:text-foreground" aria-expanded={open} onClick={() => setOpen((value) => !value)}>
<Sparkles className="size-3.5" />
<span>{isStreaming ? "正在思考" : durationSeconds ? `已思考 ${durationSeconds} 秒` : "思考过程"}</span>
<ChevronDown className={`size-3.5 transition-transform${open ? " rotate-180" : ""}`} />
</button>
{open ? <div className="mt-2 rounded-md bg-surface-active p-3 text-foreground/70"><AIMessageMarkdown isStreaming={isStreaming}>{reasoning}</AIMessageMarkdown></div> : null}
</div>;
}

View File

@@ -2,11 +2,10 @@ import React, { useEffect, useMemo, useState } from "react";
import { Dropdown } from "antd";
import { FileAudio, FileText, MoreHorizontal, Pencil, Plus, SlidersHorizontal, Sparkles, Video } from "lucide-react";
import type { CanvasFolderStyle, CanvasNodeData } from "@/types/canvas";
import { CANVAS_FOLDER_THEME_OPTIONS, resolveCanvasFolderTheme, resolveCanvasFolderThemeCover } from "@/lib/canvas/canvas-folder-theme";
import type { CanvasFolderStyle, CanvasFolderTheme, CanvasNodeData } from "@/types/canvas";
import { CanvasNodeType } from "@/types/canvas";
const DEFAULT_FOLDER_COVER = "/images/canvas/folder-default-cover.png";
const FOLDER_STYLE_OPTIONS: Array<{ key: CanvasFolderStyle; label: string }> = [
{ key: "glass", label: "流光玻璃" },
{ key: "stacked", label: "内容陈列" },
@@ -25,6 +24,7 @@ export const CanvasFolderPreview = React.memo(function CanvasFolderPreview({
onToggleCollapsed,
onTitleChange,
onStyleChange,
onThemeChange,
}: {
data: CanvasNodeData;
childNodes: CanvasNodeData[];
@@ -34,13 +34,34 @@ export const CanvasFolderPreview = React.memo(function CanvasFolderPreview({
onToggleCollapsed: (nodeId: string) => void;
onTitleChange: (nodeId: string, title: string) => void;
onStyleChange: (nodeId: string, style: CanvasFolderStyle) => void;
onThemeChange: (nodeId: string, theme: CanvasFolderTheme) => void;
}) {
const style = data.metadata?.folder?.style || "glass";
const theme = resolveCanvasFolderTheme(data.metadata?.folder?.theme);
const [editing, setEditing] = useState(false);
const [title, setTitle] = useState(data.title);
const previewNodes = useMemo(() => childNodes.filter(canPreviewFolderNode).slice(0, 3), [childNodes]);
const themeCover = data.metadata?.folder?.themeCover || DEFAULT_FOLDER_COVER;
const themeCover = resolveCanvasFolderThemeCover(theme, data.metadata?.folder?.themeCover);
const showAdd = style === "glass" || childNodes.length === 0;
const folderMenu = {
selectedKeys: [`style:${style}`, `theme:${theme}`],
items: [
{
key: "styles",
label: "文件夹样式",
children: FOLDER_STYLE_OPTIONS.map((item) => ({ key: `style:${item.key}`, label: item.label })),
},
{
key: "themes",
label: "主题皮肤",
children: CANVAS_FOLDER_THEME_OPTIONS.map((item) => ({ key: `theme:${item.key}`, label: item.label })),
},
],
onClick: ({ key }: { key: string }) => {
if (key.startsWith("style:")) onStyleChange(data.id, key.slice(6) as CanvasFolderStyle);
if (key.startsWith("theme:")) onThemeChange(data.id, key.slice(6) as CanvasFolderTheme);
},
};
useEffect(() => setTitle(data.title), [data.title]);
@@ -130,17 +151,13 @@ export const CanvasFolderPreview = React.memo(function CanvasFolderPreview({
) : (
<Dropdown
trigger={["click"]}
menu={{
selectedKeys: [style],
items: FOLDER_STYLE_OPTIONS.map((item) => ({ key: item.key, label: item.label })),
onClick: ({ key }) => onStyleChange(data.id, key as CanvasFolderStyle),
}}
menu={folderMenu}
>
<button
type="button"
className="canvas-folder-action canvas-folder-options"
aria-label={`文件夹选项,当前 ${childNodes.length} 项`}
title="切换文件夹样式"
title="切换文件夹样式与主题"
onMouseDown={(event) => event.stopPropagation()}
onClick={(event) => event.stopPropagation()}
>
@@ -153,17 +170,13 @@ export const CanvasFolderPreview = React.memo(function CanvasFolderPreview({
{!readOnly && showAdd ? (
<Dropdown
trigger={["click"]}
menu={{
selectedKeys: [style],
items: FOLDER_STYLE_OPTIONS.map((item) => ({ key: item.key, label: item.label })),
onClick: ({ key }) => onStyleChange(data.id, key as CanvasFolderStyle),
}}
menu={folderMenu}
>
<button
type="button"
className="canvas-folder-style-trigger"
aria-label="切换文件夹样式"
title="切换文件夹样式"
aria-label="切换文件夹样式与主题"
title="切换文件夹样式与主题"
onMouseDown={(event) => event.stopPropagation()}
onClick={(event) => event.stopPropagation()}
>

View File

@@ -7,7 +7,7 @@ import { CanvasFolderPreview } from "@/components/canvas/canvas-folder-preview";
import { FRAME_HEADER_HEIGHT, FRAME_PADDING, isCanvasFolderNode } from "@/lib/canvas/canvas-frame";
import { canvasThemes, type CanvasTheme } from "@/lib/canvas-theme";
import { useThemeStore } from "@/stores/use-theme-store";
import { CanvasNodeType, type CanvasFolderStyle, type CanvasNodeData, type Position } from "@/types/canvas";
import { CanvasNodeType, type CanvasFolderStyle, type CanvasFolderTheme, type CanvasNodeData, type Position } from "@/types/canvas";
type ResizeCorner = "top-left" | "top-right" | "bottom-left" | "bottom-right";
@@ -22,6 +22,7 @@ export const CanvasFrameNode = React.memo(function CanvasFrameNode({
onResize,
onToggleCollapsed,
onFolderStyleChange,
onFolderThemeChange = () => undefined,
onTitleChange,
onContextMenu,
readOnly = false,
@@ -38,6 +39,7 @@ export const CanvasFrameNode = React.memo(function CanvasFrameNode({
onResize: (nodeId: string, width: number, height: number, position?: Position) => void;
onToggleCollapsed: (nodeId: string) => void;
onFolderStyleChange: (nodeId: string, style: CanvasFolderStyle) => void;
onFolderThemeChange?: (nodeId: string, theme: CanvasFolderTheme) => void;
onTitleChange: (nodeId: string, title: string) => void;
onContextMenu: (event: ReactMouseEvent, nodeId: string) => void;
readOnly?: boolean;
@@ -194,6 +196,7 @@ export const CanvasFrameNode = React.memo(function CanvasFrameNode({
onToggleCollapsed={onToggleCollapsed}
onTitleChange={onTitleChange}
onStyleChange={onFolderStyleChange}
onThemeChange={onFolderThemeChange}
/>
) : (
<>

View File

@@ -255,6 +255,7 @@ export const CanvasResourceMentionTextarea = forwardRef<HTMLTextAreaElement, Pro
replaceEditableSelection(event.clipboardData.getData("text/plain"));
}}
onKeyDown={(event: KeyboardEvent<HTMLDivElement>) => {
if (event.key === "Enter" && (event.nativeEvent.isComposing || composingRef.current)) return;
if (mention && candidates.length) {
if (event.key === "ArrowDown") {
event.preventDefault();
@@ -339,7 +340,16 @@ export const CanvasResourceMentionTextarea = forwardRef<HTMLTextAreaElement, Pro
syncMention(next, event.target.selectionStart);
reportContentSize(event.currentTarget);
}}
onCompositionStart={(event) => {
composingRef.current = true;
props.onCompositionStart?.(event);
}}
onCompositionEnd={(event) => {
composingRef.current = false;
props.onCompositionEnd?.(event);
}}
onKeyDown={(event) => {
if (event.key === "Enter" && (event.nativeEvent.isComposing || composingRef.current)) return;
if (mention && candidates.length) {
if (event.key === "ArrowDown") {
event.preventDefault();

View File

@@ -195,8 +195,8 @@ export function CanvasToolbar({
<AnimatePresence>
{appearanceOpen ? (
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} transition={{ duration: aceternityMotion.duration.instant }} className="pointer-events-auto absolute bottom-[var(--canvas-dock-popover-offset)] z-[var(--dock-z-popover)] w-[224px] max-w-[calc(100vw-24px)] -translate-x-1/2" style={{ left: panelX || "50%" }}>
<SpotlightSurface spotlightColor={theme.toolbar.itemHover} initial={{ y: 6, scale: 0.97 }} animate={{ y: 0, scale: 1 }} exit={{ y: 4, scale: 0.98 }} transition={{ duration: aceternityMotion.duration.instant, ease: aceternityMotion.easing.enter }} className="aceternity-floating-panel overflow-hidden rounded-[var(--panel-radius)] border p-2.5 backdrop-blur-2xl" style={{ background: theme.spatial.elevated, borderColor: theme.toolbar.border, color: theme.toolbar.item }} onWheel={(event) => event.stopPropagation()}>
<motion.div initial={{ opacity: 0, scaleY: 0.9, y: 8 }} animate={{ opacity: 1, scaleY: 1, y: 0 }} exit={{ opacity: 0, scaleY: 0.92, y: 6 }} transition={{ duration: aceternityMotion.duration.panel, ease: aceternityMotion.easing.enter }} className="pointer-events-auto absolute bottom-[var(--canvas-dock-popover-offset)] z-[var(--dock-z-popover)] w-[224px] max-w-[calc(100vw-24px)]" style={{ left: panelX || "50%", transformOrigin: "bottom center", x: "-50%" }}>
<SpotlightSurface spotlightColor={theme.toolbar.itemHover} initial={{ opacity: 0, scale: 0.97 }} animate={{ opacity: 1, scale: 1 }} exit={{ opacity: 0, scale: 0.97, transition: { duration: 0 } }} transition={{ duration: aceternityMotion.duration.instant, ease: aceternityMotion.easing.enter }} className="aceternity-floating-panel overflow-hidden rounded-[var(--panel-radius)] border p-2.5 backdrop-blur-2xl" style={{ background: theme.spatial.elevated, borderColor: theme.toolbar.border, color: theme.toolbar.item }} onWheel={(event) => event.stopPropagation()}>
<PanelHeading icon={<Palette className="size-4" />} title="画布外观" subtitle="调整整个创作空间" theme={theme} />
<div className="mt-3 text-[var(--fs-micro)] font-semibold uppercase opacity-45">主题模式</div>
<div className="mt-1 grid grid-cols-2 gap-1 rounded-[var(--dock-item-radius-labeled)] border p-1" style={{ background: theme.spatial.surface, borderColor: theme.toolbar.border }}>
@@ -234,8 +234,8 @@ function AddNodeMenu({ x, theme, commands }: {
commands: CanvasCreateCommand[];
}) {
return (
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} transition={{ duration: aceternityMotion.duration.instant }} className="pointer-events-auto absolute bottom-[var(--canvas-dock-popover-offset)] z-[var(--dock-z-popover)] w-[260px] max-w-[calc(100vw-24px)] -translate-x-1/2" style={{ left: x || "50%" }}>
<SpotlightSurface spotlightColor={theme.toolbar.itemHover} initial={{ y: 6, scale: 0.97 }} animate={{ y: 0, scale: 1 }} exit={{ y: 4, scale: 0.98 }} transition={{ duration: aceternityMotion.duration.instant, ease: aceternityMotion.easing.enter }} className="aceternity-floating-panel overflow-hidden rounded-[var(--panel-radius)] border p-2 backdrop-blur-2xl" style={{ background: theme.spatial.elevated, borderColor: theme.toolbar.border, color: theme.node.text }} onWheel={(event) => event.stopPropagation()}>
<motion.div initial={{ opacity: 0, scaleY: 0.9, y: 8 }} animate={{ opacity: 1, scaleY: 1, y: 0 }} exit={{ opacity: 0, scaleY: 0.92, y: 6 }} transition={{ duration: aceternityMotion.duration.panel, ease: aceternityMotion.easing.enter }} className="pointer-events-auto absolute bottom-[var(--canvas-dock-popover-offset)] z-[var(--dock-z-popover)] w-[260px] max-w-[calc(100vw-24px)]" style={{ left: x || "50%", transformOrigin: "bottom center", x: "-50%" }}>
<SpotlightSurface spotlightColor={theme.toolbar.itemHover} initial={{ opacity: 0, scale: 0.97 }} animate={{ opacity: 1, scale: 1 }} exit={{ opacity: 0, scale: 0.97, transition: { duration: 0 } }} transition={{ duration: aceternityMotion.duration.instant, ease: aceternityMotion.easing.enter }} className="aceternity-floating-panel overflow-hidden rounded-[var(--panel-radius)] border p-2 backdrop-blur-2xl" style={{ background: theme.spatial.elevated, borderColor: theme.toolbar.border, color: theme.node.text }} onWheel={(event) => event.stopPropagation()}>
<CanvasCreateMenu commands={commands} />
</SpotlightSurface>
</motion.div>

View File

@@ -5,6 +5,7 @@ import { Button } from "antd";
import { VideoSettingsPanel, videoResolutionLabel, videoSecondsLabel, videoSizeLabel } from "@/components/video-settings-panel";
import { canvasThemes } from "@/lib/canvas-theme";
import { modelCapabilityConfigFor } from "@/lib/model-capabilities";
import { useThemeStore } from "@/stores/use-theme-store";
import type { AiConfig } from "@/stores/use-config-store";
@@ -21,7 +22,12 @@ export function CanvasVideoSettingsPopover({ config, onConfigChange, buttonClass
const panelRef = useRef<HTMLDivElement>(null);
const [open, setOpen] = useState(false);
const [buttonRect, setButtonRect] = useState<DOMRect | null>(null);
const summary = `${videoResolutionLabel(config.vquality)} · ${videoSizeLabel(config.size)} · ${videoSecondsLabel(config.videoSeconds)}`;
const resolutionSupported = Boolean(modelCapabilityConfigFor(config, config.model).video?.resolutions.length);
const summary = [
...(resolutionSupported ? [videoResolutionLabel(config.vquality)] : []),
videoSizeLabel(config.size),
videoSecondsLabel(config.videoSeconds),
].join(" · ");
useEffect(() => {
if (!open) return;

View File

@@ -85,7 +85,7 @@ export function UserOSSSettingsForm() {
<Cloud className="size-4" />
我的对象存储
</div>
<p className="mt-1 max-w-3xl text-xs leading-5 text-foreground/55">支持阿里云 OSS 与腾讯云 COS。启用后,新上传和新生成的媒体优先写入你的存储桶;停用时回退到平台存储。历史资源固定使用创建时的配置版本。</p>
<p className="mt-1 max-w-3xl text-xs leading-5 text-foreground/55">支持阿里云 OSS 与腾讯云 COS。启用后,新上传和新生成的媒体优先写入你的存储桶;停用时回退到平台存储。历史资源继续使用创建时的存储与密钥配置,同一存储位置的 CDN 域名会跟随当前配置。</p>
</div>
<div className="flex shrink-0 gap-2">
<Tag color={setting?.enabled ? "success" : "default"}>{setting?.enabled ? "已启用" : "未启用"}</Tag>

View File

@@ -3,9 +3,9 @@ import type { ReactNode } from "react";
import { defaultImageCapabilityConfig, defaultModelCapabilityConfig, type ImageCapabilityConfig, type ModelCapabilityConfig, type VideoCapabilityConfig } from "@/lib/model-capabilities";
import type { ModelProtocol } from "@/lib/model-protocols";
import { VIDEO_RESOLUTION_CAPABILITY_OPTIONS } from "@/lib/video-generation-options";
const ratioOptions = ["16:9", "9:16", "1:1", "4:3", "3:4", "21:9"];
const resolutionOptions = ["480p", "720p", "1080p", "2160p"];
const operationOptions = [
{ label: "文生视频", value: "text_to_video" },
{ label: "图生视频", value: "image_to_video" },
@@ -35,6 +35,7 @@ export function ModelCapabilityEditor({ value, onChange, protocol, capability =
const updateReferences = (patch: Partial<VideoCapabilityConfig["references"]>) => update({ references: { ...profile.references, ...patch } });
const updateDuration = (patch: Partial<VideoCapabilityConfig["duration"]>) => update({ duration: { ...profile.duration, ...patch } });
const durationValues = (profile.duration.values || []).join(",");
const resolutionOptions = Array.from(new Set([...VIDEO_RESOLUTION_CAPABILITY_OPTIONS, ...profile.resolutions]));
return (
<div className="space-y-3 rounded-md border border-border/70 bg-muted/10 p-3">
@@ -46,7 +47,8 @@ export function ModelCapabilityEditor({ value, onChange, protocol, capability =
<CapabilityGroup title="引用限制">
<div className="grid grid-cols-2 gap-2 sm:grid-cols-4">
<NumberField label="提示词字符数" value={profile.references.promptMaxChars} min={1} disabled={disabled} onChange={(value) => updateReferences({ promptMaxChars: value || 1 })} />
<NumberField label="最大图片引用" value={profile.references.maxImages} min={0} disabled={disabled} onChange={(value) => updateReferences({ maxImages: value || 0 })} />
<NumberField label="最少图片引用" value={profile.references.minImages} min={0} max={profile.references.maxImages} disabled={disabled} onChange={(value) => updateReferences({ minImages: value || 0 })} />
<NumberField label="最大图片引用" value={profile.references.maxImages} min={0} disabled={disabled} onChange={(value) => { const maxImages = value || 0; updateReferences({ maxImages, minImages: Math.min(profile.references.minImages, maxImages) }); }} />
<NumberField label="图片上限 MB" value={bytesToMB(profile.references.maxImageBytes)} min={0} disabled={disabled} onChange={(value) => updateReferences({ maxImageBytes: mbToBytes(value) })} />
<NumberField label="最大视频引用" value={profile.references.maxVideos} min={0} disabled={disabled} onChange={(value) => updateReferences({ maxVideos: value || 0 })} />
<NumberField label="视频上限 MB" value={bytesToMB(profile.references.maxVideoBytes)} min={0} disabled={disabled} onChange={(value) => updateReferences({ maxVideoBytes: mbToBytes(value) })} />
@@ -75,7 +77,7 @@ export function ModelCapabilityEditor({ value, onChange, protocol, capability =
<div className="grid gap-2 sm:grid-cols-2">
<Field label="画面比例"><Select mode="multiple" className="w-full" disabled={disabled} value={profile.ratios} options={ratioOptions.map((item) => ({ label: item, value: item }))} onChange={(ratios) => update({ ratios, defaultRatio: ratios.includes(profile.defaultRatio) ? profile.defaultRatio : ratios[0] || "16:9" })} /></Field>
<Field label="默认比例"><Select className="w-full" disabled={disabled} value={profile.defaultRatio} options={profile.ratios.map((item) => ({ label: item, value: item }))} onChange={(defaultRatio) => update({ defaultRatio })} /></Field>
<Field label="输出分辨率"><Select mode="multiple" className="w-full" disabled={disabled} value={profile.resolutions} options={resolutionOptions.map((item) => ({ label: item.toUpperCase(), value: item }))} onChange={(resolutions) => update({ resolutions, defaultResolution: resolutions.includes(profile.defaultResolution) ? profile.defaultResolution : resolutions[0] || "720p" })} /></Field>
<Field label="输出分辨率"><Select mode="tags" className="w-full" disabled={disabled} value={profile.resolutions} tokenSeparators={[","]} placeholder="选择标准档位或输入 768p 等模型专属值" options={resolutionOptions.map((item) => ({ label: item.toUpperCase(), value: item }))} onChange={(resolutions) => update({ resolutions, defaultResolution: resolutions.includes(profile.defaultResolution) ? profile.defaultResolution : resolutions[0] || "" })} /></Field>
<Field label="默认分辨率"><Select className="w-full" disabled={disabled} value={profile.defaultResolution} options={profile.resolutions.map((item) => ({ label: item.toUpperCase(), value: item }))} onChange={(defaultResolution) => update({ defaultResolution })} /></Field>
</div>
<div className="grid gap-2 sm:grid-cols-2">
@@ -162,8 +164,8 @@ function Field({ label, children }: { label: string; children: ReactNode }) {
return <label className="block min-w-0"><span className="mb-1 block text-[var(--fs-tiny)] text-foreground/48">{label}</span>{children}</label>;
}
function NumberField({ label, value, min, disabled, onChange }: { label: string; value?: number; min: number; disabled: boolean; onChange: (value: number | null) => void }) {
return <Field label={label}><InputNumber className="w-full" disabled={disabled} min={min} precision={0} value={value} onChange={onChange} /></Field>;
function NumberField({ label, value, min, max, disabled, onChange }: { label: string; value?: number; min: number; max?: number; disabled: boolean; onChange: (value: number | null) => void }) {
return <Field label={label}><InputNumber className="w-full" disabled={disabled} min={min} max={max} precision={0} value={value} onChange={onChange} /></Field>;
}
function BooleanField({ label, value, disabled, onChange }: { label: string; value: { supported: boolean; default: boolean }; disabled: boolean; onChange: (value: { supported: boolean; default: boolean }) => void }) {

View File

@@ -40,9 +40,11 @@ type DockMetrics = {
distance: number;
};
// magnification 收敛:base 略增以适配圆角方块,magnified 幅度从 +14 降到 +8,
// 避免 dock 整体跳动,同时保留 Aceternity 接近放大身份。
const DOCK_METRICS: Record<NonNullable<FloatingDockProps["size"]>, DockMetrics> = {
default: { base: 29, magnified: 43, icon: 14, iconMagnified: 19, distance: 108 },
compact: { base: 25, magnified: 35, icon: 13, iconMagnified: 17, distance: 88 },
default: { base: 30, magnified: 38, icon: 15, iconMagnified: 18, distance: 100 },
compact: { base: 26, magnified: 32, icon: 13, iconMagnified: 16, distance: 84 },
};
const TOUCH_DOCK_METRICS: Record<NonNullable<FloatingDockProps["size"]>, DockMetrics> = {
@@ -228,7 +230,7 @@ function DockCommandButton({ command, mouseX, metrics, motionEnabled, compact, s
aria-expanded={command.expands ? command.active || undefined : undefined}
aria-pressed={command.expands ? undefined : command.active || undefined}
disabled={command.disabled}
className={cn("aceternity-dock-command group relative grid size-full place-items-center rounded-full border outline-none", command.quiet && "is-quiet", command.active && "is-active", command.danger && "is-danger")}
className={cn("aceternity-dock-command group relative grid size-full place-items-center rounded-[var(--dock-item-radius)] border outline-none", command.quiet && "is-quiet", command.active && "is-active", command.danger && "is-danger")}
whileTap={motionEnabled && !command.disabled ? { scale: 0.92 } : undefined}
transition={aceternityMotion.spring.dock}
onMouseEnter={() => setHovered(true)}

View File

@@ -50,7 +50,7 @@ export function VideoSettingsPanel({ config, onConfigChange, theme, showTitle =
<ImageSettingsTheme theme={theme}>
<div className={className} style={{ color: theme.node.text }} onMouseDown={(event) => event.stopPropagation()}>
{showTitle ? <div className="text-sm font-semibold">视频设置</div> : null}
<SettingGroup title="分辨率" color={theme.node.muted}>
{configuredResolutions.length ? <SettingGroup title="分辨率" color={theme.node.muted}>
<div className="grid grid-cols-3 gap-1.5">
{configuredResolutions.map((item) => (
<OptionPill key={item.value} selected={resolution === item.value} theme={theme} onClick={() => onConfigChange("vquality", item.value)}>
@@ -58,7 +58,7 @@ export function VideoSettingsPanel({ config, onConfigChange, theme, showTitle =
</OptionPill>
))}
</div>
</SettingGroup>
</SettingGroup> : null}
<SettingGroup title="尺寸" color={theme.node.muted}>
<div className="grid grid-cols-[1fr_auto_1fr] items-center gap-1.5">
<DimensionInput prefix="W" value={dimensions.width} disabled={size === "auto"} theme={theme} onChange={(value) => updateDimension("width", value)} />