feat: refresh home and canvas UI with customizable image toolbar

This commit is contained in:
134 2026-10-04 03:37:06 +08:00
parent 2c8f306e74
commit d376f5959f
81 changed files with 1899 additions and 203 deletions

View File

@ -87,6 +87,7 @@ import {
type CanvasAddImageDialogImage,
} from "@/components/canvas/CanvasAddImageDialog";
import { CanvasToolbarIcon } from "@/components/canvas/CanvasToolbarIcon";
import { CanvasSurfaceHeader } from "@/components/canvas/CanvasSurfaceHeader";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
@ -262,7 +263,7 @@ import type {
const CANVAS_SITE_SETTINGS_REFRESH_INTERVAL_MS = 15000;
const CANVAS_SITE_SETTINGS_TIMEOUT_MS = 10000;
const CANVAS_SURFACE_BACKGROUND = "#f4f4f5";
const CANVAS_SURFACE_BACKGROUND = "#f5f5f5";
const fetchCanvasSiteSettings = async (url: string) => {
const separator = url.includes("?") ? "&" : "?";
@ -3141,8 +3142,7 @@ export default function CanvasEditorLegacyPage({
const updateWidthFromClientX = (clientX: number) => {
const currentViewportWidth = window.innerWidth;
const nextWidth =
currentViewportWidth - DOCKED_PANEL_INSET - clientX;
const nextWidth = currentViewportWidth - DOCKED_PANEL_INSET - clientX;
setFloatingPanelWidth(
clampFloatingPanelWidth(nextWidth, currentViewportWidth)
@ -10312,7 +10312,8 @@ export default function CanvasEditorLegacyPage({
return (
<TooltipProvider>
<div
className="relative h-dvh w-full overflow-hidden overscroll-none"
data-canvas-panel-open={isPanelOpen ? "true" : "false"}
className={cn("relative h-dvh w-full overflow-hidden overscroll-none", !isVideoMode && "lovart-canvas-surface")}
style={
{
"--canvas-surface-background": canvasSurfaceBackground,
@ -10324,6 +10325,7 @@ export default function CanvasEditorLegacyPage({
<div
className="absolute inset-0 z-0"
data-canvas-stage="true"
style={isVideoMode ? undefined : { right: panelOcclusionRight, top: 80 }}
key={`workspace-${id}`}
>
<CanvasWorkspace
@ -10334,9 +10336,10 @@ export default function CanvasEditorLegacyPage({
? "先在右侧选择文生视频、主体参考或首尾帧模式,再开始生成"
: '点击顶部"添加"按钮开始创作'
}
occlusionRight={panelOcclusionRight}
occlusionRight={0}
onBlankCanvasDoubleClick={handleOpenVideoCanvasInsertMenu}
showEmptyState={!isVideoMode}
showEmptyState={false}
showZoomControls={isVideoMode}
workspaceLabel={workspaceLabel}
workspaceVariant={isVideoMode ? "video" : "canvas"}
layerSplitEnabled={isLayerSplitEnabled}
@ -11025,14 +11028,27 @@ export default function CanvasEditorLegacyPage({
</div>
) : null}
{brandEntry}
{isVideoMode ? brandEntry : (
<CanvasSurfaceHeader
isSaving={isSaving}
onAddImage={handleAddImageNode}
onHome={goHome}
onOpenFiles={() => {
setIsPanelOpen(true);
setPanelMode("fileList");
}}
onSave={() => void handleSave()}
projectTitle={projectTitle}
rightInset={panelOcclusionRight}
/>
)}
{agentDisabledNotice}
{/* 2. Floating Actions (Top Right) */}
{!isVideoMode && !isPanelOpen && (
<div
className="absolute z-20"
style={{ top: FLOATING_PANEL_INSET, right: FLOATING_PANEL_INSET }}
style={{ top: 46, right: 12 }}
>
<Button
className={cn(
@ -11064,7 +11080,7 @@ export default function CanvasEditorLegacyPage({
)}
data-canvas-right-panel="true"
style={{
top: DOCKED_PANEL_INSET,
top: 40,
right: DOCKED_PANEL_INSET,
bottom: DOCKED_PANEL_INSET,
width: resolvedPanelWidth,
@ -11095,7 +11111,7 @@ export default function CanvasEditorLegacyPage({
/>
</div>
) : null}
<div className="flex h-full w-full flex-col overflow-hidden border-l bg-white dark:bg-zinc-950">
<div className="flex h-full w-full flex-col overflow-hidden border-l border-[#ededed] bg-white dark:bg-zinc-950">
{!areSecondaryPanelsReady ? (
<CanvasLazyPanelLoader label="正在准备侧边面板..." />
) : panelMode === "chat" ? (
@ -11201,11 +11217,11 @@ export default function CanvasEditorLegacyPage({
className={cn(
isVideoMode
? VIDEO_CANVAS_TOOLBAR_CLASS
: "border bg-white shadow-2xl dark:bg-zinc-950",
: "border border-[#e9e9e9] bg-white shadow-[0_1px_3px_rgba(0,0,0,0.035)] dark:border-white/10 dark:bg-zinc-950",
!isVideoMode &&
(shouldDockFloatingToolbarToSide
? "flex flex-col items-center gap-1 rounded-[24px] px-1.5 py-2.5"
: "flex items-center gap-1 rounded-[24px] px-2.5 py-1.5")
: "flex items-center gap-1 rounded-[14px] px-1.5 py-1.5")
)}
style={
shouldDockFloatingToolbarToSide
@ -11223,7 +11239,7 @@ export default function CanvasEditorLegacyPage({
tool === "select" &&
(isVideoMode
? VIDEO_CANVAS_TOOLBAR_ACTIVE_BUTTON_CLASS
: "bg-primary text-primary-foreground hover:bg-primary/90")
: "bg-[#ededed] text-[#333333] hover:bg-[#e5e5e5] dark:bg-white/12 dark:text-white")
)}
onClick={() => setTool("select")}
size="icon"
@ -11233,6 +11249,8 @@ export default function CanvasEditorLegacyPage({
active={tool === "select"}
className="h-4 w-4"
fallback={MousePointer}
referenceName={isVideoMode ? undefined : "cursor"}
referenceSize={20}
item={getCanvasToolbarItem(
canvasToolbarConfig,
"selectTool"
@ -11259,6 +11277,8 @@ export default function CanvasEditorLegacyPage({
<CanvasToolbarIcon
className="h-4 w-4"
fallback={ImagePlus}
referenceName={isVideoMode ? undefined : "image"}
referenceSize={18}
item={getCanvasToolbarItem(
canvasToolbarConfig,
"addAgentImageSlot"
@ -11313,7 +11333,7 @@ export default function CanvasEditorLegacyPage({
tool === "pan" &&
(isVideoMode
? VIDEO_CANVAS_TOOLBAR_ACTIVE_BUTTON_CLASS
: "bg-primary text-primary-foreground hover:bg-primary/90")
: "bg-[#ededed] text-[#333333] hover:bg-[#e5e5e5] dark:bg-white/12 dark:text-white")
)}
onClick={() => setTool("pan")}
size="icon"
@ -11443,7 +11463,7 @@ export default function CanvasEditorLegacyPage({
tool === "draw" &&
(isVideoMode
? VIDEO_CANVAS_TOOLBAR_ACTIVE_BUTTON_CLASS
: "bg-primary text-primary-foreground hover:bg-primary/90")
: "bg-[#ededed] text-[#333333] hover:bg-[#e5e5e5] dark:bg-white/12 dark:text-white")
)}
onClick={() => setTool("draw")}
size="icon"
@ -11484,7 +11504,7 @@ export default function CanvasEditorLegacyPage({
isAgentMode &&
(isVideoMode
? VIDEO_CANVAS_TOOLBAR_ACTIVE_BUTTON_CLASS
: "bg-primary text-primary-foreground hover:bg-primary/90")
: "bg-[#ededed] text-[#333333] hover:bg-[#e5e5e5] dark:bg-white/12 dark:text-white")
)}
onClick={handleStyleModeToolbarClick}
size="icon"
@ -11558,6 +11578,8 @@ export default function CanvasEditorLegacyPage({
<CanvasToolbarIcon
className="h-4 w-4"
fallback={Layers}
referenceName={isVideoMode ? undefined : "layers"}
referenceSize={18}
item={getCanvasToolbarItem(
canvasToolbarConfig,
"batchProcessing"
@ -11589,6 +11611,8 @@ export default function CanvasEditorLegacyPage({
<CanvasToolbarIcon
className="h-4 w-4"
fallback={ImagePlus}
referenceName={isVideoMode ? undefined : "image"}
referenceSize={18}
item={getCanvasToolbarItem(
canvasToolbarConfig,
isVideoMode ? "addImageInput" : "addImage"
@ -11878,6 +11902,8 @@ export default function CanvasEditorLegacyPage({
<CanvasToolbarIcon
className="h-4 w-4"
fallback={Box}
referenceName={isVideoMode ? undefined : "model-auto"}
referenceSize={18}
item={getCanvasToolbarItem(canvasToolbarConfig, "model3d")}
/>
</Button>
@ -11967,6 +11993,8 @@ export default function CanvasEditorLegacyPage({
<CanvasToolbarIcon
className="h-4 w-4"
fallback={Plus}
referenceName={isVideoMode ? undefined : "plus"}
referenceSize={18}
item={getCanvasToolbarItem(
canvasToolbarConfig,
isVideoMode ? "newVideoCanvas" : "newCanvas"
@ -11994,12 +12022,15 @@ export default function CanvasEditorLegacyPage({
type="file"
/>
{/* 6. Bottom Left Compact Controls */}
{/* Compact zoom and history controls. */}
<div
data-canvas-compact-controls="true"
className="absolute z-20"
style={{
bottom: `${FLOATING_BOTTOM_INSET + 4}px`,
left: `${compactControlsLeft + 8}px`,
...(isVideoMode
? { left: `${compactControlsLeft + 8}px` }
: { right: `${panelOcclusionRight + 16}px` }),
}}
>
<div

View File

@ -519,3 +519,250 @@
padding: 0 12px 12px;
}
}
/* Lovart surface typography and controls, scoped to the home and image canvas. */
.lovart-home-surface,
.lovart-canvas-surface {
font-family: Arial, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif;
-webkit-font-smoothing: antialiased;
}
.lovart-suggestion {
display: inline-flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
gap: 8px;
height: 32px;
padding: 0 14px;
border: 1px solid #e5e5e5;
border-radius: 999px;
background: white;
color: #333;
font-size: 13px;
white-space: nowrap;
transition: background-color 150ms, border-color 150ms;
}
.lovart-suggestion:hover { background: #f7f7f7; border-color: #d5d5d5; }
.lovart-suggestion:focus-visible,
.lovart-start-action:focus-visible { outline: 2px solid #8c8c8c; outline-offset: 3px; }
.lovart-start-action {
display: inline-flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
gap: 9px;
min-width: 172px;
height: 40px;
padding: 0 16px;
border: 1px solid #e5e5e5;
border-radius: 999px;
background: white;
color: #333;
font-size: 14px;
font-weight: 400;
box-shadow: 0 1px 2px rgb(0 0 0 / 4%);
transition: background-color 150ms, border-color 150ms;
}
.lovart-start-action:hover { background: #f7f7f7; border-color: #d4d4d4; }
.lovart-canvas-surface [data-lovart-chat="true"] .lovart-chat-header { border-bottom: 1px solid #ededed; }
.lovart-canvas-surface [data-lovart-chat="true"] .lovart-chat-composer { padding: 0 8px 8px; }
.lovart-canvas-surface [data-lovart-chat="true"] .lovart-chat-composer > [data-canvas-chat-input-anchor] {
min-height: 120px;
border-color: #e5e5e5;
box-shadow: 0 1px 3px rgb(0 0 0 / 5%);
}
/* The promo backplate continues underneath the composer's rounded corners. */
.lovart-chat-promo-connected { padding: 0 8px; }
.lovart-chat-promo-surface { padding-bottom: 24px; border-radius: 24px 24px 0 0; background: #ebebeb; }
.lovart-chat-promo-row { display: flex; align-items: center; gap: 8px; height: 36px; padding: 0 10px 0 16px; }
.lovart-chat-promo-message { display: flex; align-items: center; gap: 8px; min-width: 0; flex: 1; padding: 0; border: 0; background: transparent; color: #202020; font-size: 14px; font-weight: 400; line-height: 20px; text-align: left; cursor: pointer; }
.lovart-chat-promo-message:disabled { cursor: default; opacity: 1; }
.lovart-chat-promo-message > span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.lovart-chat-promo-badge { flex-shrink: 0; }
.lovart-chat-promo-close { display: flex; align-items: center; justify-content: center; flex-shrink: 0; width: 28px; height: 28px; border: 0; border-radius: 50%; background: transparent; color: #262626; cursor: pointer; }
.lovart-chat-promo-close:hover { background: rgb(0 0 0 / 5%); }
.lovart-canvas-surface [data-lovart-chat="true"] .lovart-chat-promo-connected + .lovart-chat-composer { position: relative; z-index: 1; margin-top: -24px; background: transparent; }
.lovart-canvas-surface [data-lovart-chat="true"] .lovart-chat-composer > [data-canvas-chat-input-anchor][data-canvas-composer-mode="image"] { min-height: 184px; border-radius: 24px; }
.lovart-canvas-surface [data-lovart-chat="true"] .lovart-reference-upload { margin: 8px 8px 0; }
.lovart-canvas-surface [data-lovart-chat="true"] .lovart-reference-tile { flex-direction: column; gap: 8px; width: 56px; height: 68px; border: 0; border-radius: 18px; background: #f5f5f5; color: #aaa; font-size: 12px; font-weight: 400; }
.lovart-canvas-surface [data-lovart-chat="true"] .lovart-reference-tile:hover { background: #ececec; color: #777; }
.lovart-canvas-surface [data-lovart-chat="true"] .lovart-reference-tile > svg { width: 14px; height: 14px; stroke-width: 1.5; }
.lovart-canvas-surface [data-lovart-chat="true"] [data-canvas-composer-mode="image"] .lovart-composer-text-region { padding: 12px 16px 0; }
.lovart-canvas-surface [data-lovart-chat="true"] [data-canvas-composer-mode="image"] [role="textbox"] { min-height: 40px; font-size: 14px; }
.lovart-canvas-surface [data-lovart-chat="true"] [data-canvas-composer-mode="image"] [role="textbox"]:empty::before { color: #b0b0b0; }
.dark .lovart-chat-promo-surface { background: #27272a; }
.dark .lovart-chat-promo-message, .dark .lovart-chat-promo-close { color: #eee; }
.dark .lovart-chat-promo-close:hover { background: rgb(255 255 255 / 10%); }
.dark .lovart-canvas-surface [data-lovart-chat="true"] .lovart-reference-tile { background: #27272a; color: #a1a1aa; }
.dark .lovart-canvas-surface [data-lovart-chat="true"] .lovart-reference-tile:hover { background: #3f3f46; color: #eee; }
.dark .lovart-suggestion,
.dark .lovart-start-action { border-color: rgb(255 255 255 / 12%); background: #18181b; color: #eee; }
.dark .lovart-suggestion:hover,
.dark .lovart-start-action:hover { background: #27272a; border-color: rgb(255 255 255 / 22%); }
.dark .lovart-canvas-surface [data-lovart-chat="true"] .lovart-chat-header { border-color: rgb(255 255 255 / 10%); }
.dark .lovart-canvas-surface [data-lovart-chat="true"] .lovart-chat-composer > [data-canvas-chat-input-anchor] { border-color: rgb(255 255 255 / 12%); }
@media (max-width: 640px) {
.lovart-home-surface main { padding-top: 112px; }
.lovart-hero { margin-top: 16px; }
.lovart-home-composer { border-radius: 20px; }
.lovart-suggestion { padding: 0 11px; font-size: 12px; }
.lovart-start-action { min-width: 160px; }
.lovart-chat-welcome h1 { font-size: 15px; }
}
@media (prefers-reduced-motion: reduce) {
.lovart-home-surface *, .lovart-canvas-surface * { scroll-behavior: auto !important; }
}
.lovart-home-surface :is(button, input, textarea, select),
.lovart-canvas-surface :is(button, input, textarea, select) { font-family: inherit; }
@media (max-width: 640px) {
.lovart-canvas-surface[data-canvas-panel-open="true"] :is([data-canvas-topbar], [data-canvas-floating-toolbar], [data-canvas-compact-controls]) { display: none; }
}
/* Reference icon sizing is independent of the application's root rem scale. */
.lovart-suggestion { gap: 8px; padding: 0 12px; }
.lovart-start-action { min-width: 172.71875px; gap: 8px; padding: 0 12px; }
.lovart-canvas-surface [data-canvas-floating-toolbar] button { min-width: 32px; height: 32px; border-radius: 8px; }
.lovart-canvas-surface [data-lovart-chat="true"] [data-canvas-composer-mode="image"] .lovart-composer-mode-selector { width: 90px; height: 32px; padding: 0 8px; background: transparent; font-size: 14px; font-weight: 400; }
.lovart-canvas-surface [data-lovart-chat="true"] [data-canvas-composer-mode="image"] .lovart-composer-mode-selector > span { font-size: 13px; }
.lovart-canvas-surface [data-lovart-chat="true"] [data-canvas-composer-mode="image"] .lovart-composer-mode-selector:hover,
.lovart-canvas-surface [data-lovart-chat="true"] .lovart-image-params:hover,
.lovart-canvas-surface [data-lovart-chat="true"] .lovart-model-selector:hover { background: rgb(0 0 0 / 4%); }
.lovart-canvas-surface [data-lovart-chat="true"] .lovart-image-params { width: 105.3828125px; min-width: 105.3828125px; height: 32px; padding: 0 4px 0 12px; background: transparent; font-size: 13px; gap: 4px; }
.lovart-canvas-surface [data-lovart-chat="true"] .lovart-model-selector { flex: 0 0 121px; width: 121px; height: 32px; padding: 0 4px 0 8px; gap: 0; border: 0; background: transparent; box-shadow: none; font-size: 13px; font-weight: 400; }
.lovart-canvas-surface [data-lovart-chat="true"] .lovart-model-selector > [data-reference-icon]:first-child { margin-right: 8px; }
.lovart-canvas-surface [data-lovart-chat="true"] .lovart-model-selector > span:not([data-reference-icon]) { max-width: 65px; }
.dark .lovart-canvas-surface [data-lovart-chat="true"] :is(.lovart-model-selector, .lovart-image-params) { color: #eee; }
.dark .lovart-canvas-surface [data-lovart-chat="true"] :is(.lovart-model-selector, .lovart-image-params, .lovart-composer-mode-selector):hover { background: rgb(255 255 255 / 8%); }
.lovart-canvas-surface [data-lovart-chat="true"] [data-canvas-composer-mode="image"] .lovart-composer-footer { padding: 0 8px 8px; gap: 0; }
.lovart-canvas-surface [data-lovart-chat="true"] [data-canvas-composer-mode="image"] .lovart-composer-footer > div:first-child { flex: 0 0 90px; padding-right: 0; overflow: visible; }
.lovart-canvas-surface [data-lovart-chat="true"] [data-canvas-composer-mode="image"] .lovart-composer-footer > div:last-child { flex: 1 1 0; min-width: 0; gap: 0; }
.lovart-canvas-surface [data-lovart-chat="true"] [data-canvas-composer-mode="image"] .lovart-composer-send { min-width: 32px; width: auto; height: 32px; padding: 0 8px; gap: 2px; font-size: 13px; font-weight: 400; }
/* The existing image toolbar is scaled to 80%; these dimensions render at
the reference's 44px bar, 32px buttons, and 20px icons. */
.lovart-image-action-bar {
height: 55px;
gap: 5px;
padding: 0 7.5px;
border: 1.25px solid rgba(0, 0, 0, 0.07);
border-radius: 17.5px;
background: #fff !important;
color: #000;
box-shadow: 0 1.25px 2.5px rgba(0, 0, 0, 0.04);
backdrop-filter: none;
}
.lovart-image-action-bar::before { display: none; }
.lovart-image-action-bar > button {
width: 40px;
min-width: 40px;
height: 40px;
margin: 0;
gap: 0;
padding: 0;
border-radius: 10px;
background: transparent;
color: #000;
font-size: 0;
font-weight: 400;
box-shadow: none;
}
.lovart-image-action-bar > button:hover,
.lovart-image-action-bar > button[data-state="open"],
.lovart-image-action-bar > button[data-reference-active="true"] {
background: rgba(0, 0, 0, 0.04);
color: #000;
}
.lovart-image-action-bar > button:disabled {
background: transparent;
color: rgba(0, 0, 0, 0.3);
}
.lovart-image-action-bar > button > svg,
.lovart-image-action-bar > button > [data-reference-icon],
.lovart-image-action-bar > button > span[aria-hidden="true"],
.lovart-image-action-bar > button > img {
width: 25px;
height: 25px;
margin: 0;
flex-shrink: 0;
}
.lovart-image-action-bar > button > img { filter: brightness(0); }
.lovart-image-action-bar > div.h-px,
.lovart-image-action-bar > div.w-px {
width: 1.25px;
height: 55px;
margin: 0 2.5px;
background: rgba(0, 0, 0, 0.07);
}
.lovart-image-action-menu [role="menuitem"] > svg,
.lovart-image-action-menu [role="menuitem"] > [data-reference-icon] {
width: 18px;
height: 18px;
}
.dark .lovart-image-action-bar {
border-color: rgba(255, 255, 255, 0.1);
background: #18191c !important;
color: #fff;
}
.dark .lovart-image-action-bar > button { color: #fff; }
.dark .lovart-image-action-bar > button:hover,
.dark .lovart-image-action-bar > button[data-state="open"],
.dark .lovart-image-action-bar > button[data-reference-active="true"] {
background: rgba(255, 255, 255, 0.08);
color: #fff;
}
.dark .lovart-image-action-bar > button:disabled { color: rgba(255, 255, 255, 0.3); }
.dark .lovart-image-action-bar > button > img { filter: brightness(0) invert(1); }
.dark .lovart-image-action-bar > div.w-px { background: rgba(255, 255, 255, 0.1); }
.lovart-image-action-bar[data-show-tool-names="true"] > button[data-tool-key] { width: auto; gap: 7.5px; padding: 0 12.5px; font-size: 16.25px; }
.lovart-image-tool-label { white-space: nowrap; }
.lovart-image-tool-tooltip { z-index: 10030; border: 0; border-radius: 6px; background: #1b1b1b; padding: 6px 9px; color: #fff; font-size: 13px; line-height: 18px; box-shadow: 0 4px 10px #00000012; --tw-ring-shadow: 0 0 transparent; }
.lovart-image-custom-more { min-width: 172px; padding: 6px 12px; border: 1px solid #e8e8e8; border-radius: 12px; background: #fff; }
.lovart-image-custom-more [role="menuitem"] { min-height: 32px; gap: 10px; padding: 4px 0; font-size: 14px; font-weight: 400; }
.lovart-image-custom-more .lovart-customize-entry { justify-content: center; gap: 5px; }
.lovart-toolbar-customizer-overlay { z-index: 10010; background: #0006; }
.lovart-toolbar-customizer { z-index: 10020; display: flex; flex-direction: column; width: min(720px, calc(100vw - 32px)); max-width: 720px; max-height: calc(100dvh - 48px); gap: 0; padding: 20px 24px 24px; border: 1px solid #e5e5e5; border-radius: 16px; background: #fff; color: #242424; box-shadow: 0 8px 24px #00000012; }
.lovart-customizer-header { display: flex; flex-shrink: 0; justify-content: space-between; align-items: flex-start; gap: 16px; }
.lovart-customizer-header h2 { margin: 0; font-size: 18px; line-height: 24px; font-weight: 600; }
.lovart-customizer-header p { margin-top: 2px; color: inherit; font-size: 14px; line-height: 22px; }
.lovart-customizer-header-actions { display: flex; gap: 4px; }
.lovart-customizer-header-actions button { display: flex; width: 28px; height: 28px; align-items: center; justify-content: center; border-radius: 7px; }
.lovart-customizer-header-actions button:last-child { background: #f1f1f1; }
.lovart-customizer-header-actions button:hover { background: #e9e9e9; }
.lovart-customizer-body { min-height: 0; overflow-y: auto; margin: 24px 0 20px; }
.lovart-customizer-preview { display: flex; height: 180px; align-items: center; overflow: hidden; padding: 16px; border-radius: 10px; background: #e5edef url("/icons/lovart/toolbar-preview.png") center / cover; }
.lovart-customizer-preview-bar { display: flex; align-items: center; flex: 1; min-width: 0; gap: 4px; padding: 5px 8px; border: 1px solid #e5e5e5; border-radius: 14px; background: #fff; overflow-x: auto; scrollbar-width: thin; }
.lovart-preview-fixed,.lovart-preview-chip { display: flex; align-items: center; flex-shrink: 0; height: 32px; gap: 8px; padding: 0 10px; font-size: 13px; white-space: nowrap; }
.lovart-preview-chip { position: relative; margin-top: 5px; margin-bottom: 5px; border-radius: 8px; background: #f4f4f4; cursor: grab; }
.lovart-preview-chip:active { cursor: grabbing; }
.lovart-preview-chip > button:last-child { position: absolute; top: -5px; right: -3px; display: flex; width: 16px; height: 16px; align-items: center; justify-content: center; border: 1px solid #e6e6e6; border-radius: 50%; background: #fff; color: #888; }
.lovart-customizer-tools { display: grid; grid-template-columns: repeat(3,minmax(0,1fr)); gap: 8px; margin-top: 24px; }
.lovart-customizer-tools > button { display: flex; align-items: center; gap: 10px; height: 36px; min-width: 0; padding: 0 12px; border: 1px solid #e2e2e2; border-radius: 8px; background: #fff; font-size: 14px; text-align: left; }
.lovart-customizer-tools > button > span:not([aria-hidden]) { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.lovart-customizer-tools > button > svg:last-child { flex-shrink: 0; }
.lovart-customizer-tools > button:hover { background: #f7f7f7; }
.lovart-customizer-footer { display: flex; flex-shrink: 0; align-items: center; justify-content: space-between; gap: 16px; }
.lovart-customizer-footer > label,.lovart-customizer-footer > div { display: flex; align-items: center; gap: 8px; font-size: 14px; }
.lovart-customizer-switch { width: 32px; height: 18px; padding: 2px; border-radius: 20px; background: #d5d5d5; }
.lovart-customizer-switch > span { display: block; width: 14px; height: 14px; border-radius: 50%; background: #fff; transition: transform .15s; }
.lovart-customizer-switch[aria-checked="true"] { background: #2c2c2c; }
.lovart-customizer-switch[aria-checked="true"] > span { transform: translateX(14px); }
.lovart-customizer-footer > div > button { width: 80px; height: 36px; border: 1px solid #e3e3e3; border-radius: 8px; font-size: 14px; }
.lovart-customizer-footer .lovart-customizer-save { border-color: #2c2c2c; background: #2c2c2c; color: #fff; }
.lovart-customizer-footer > div > button:hover { opacity: .8; }
.lovart-toolbar-customizer button:focus-visible,.lovart-preview-chip:focus-visible { outline: 2px solid #397dff; outline-offset: 2px; }
.dark .lovart-toolbar-customizer,.dark .lovart-image-custom-more { background: #202124; border-color: #3d3e40; color: #f5f5f5; }
.dark .lovart-customizer-preview-bar,.dark .lovart-customizer-tools > button { background: #26272a; border-color: #46474a; }
.dark .lovart-preview-chip,.dark .lovart-customizer-header-actions button:last-child { background: #35363a; }
.dark .lovart-preview-chip > button:last-child { background: #26272a; border-color: #46474a; color: #ccc; }
.dark .lovart-customizer-tools > button:hover { background: #35363a; }
@media(max-width:600px) { .lovart-toolbar-customizer { padding: 18px; } .lovart-customizer-tools { grid-template-columns: repeat(2,minmax(0,1fr)); } .lovart-customizer-preview { height: 140px; } }
.lovart-preview-handle { display: flex; align-items: center; gap: 8px; white-space: nowrap; cursor: grab; }
.lovart-image-tool-tooltip { display: flex; align-items: center; gap: 8px; }
.lovart-image-tool-cost { margin-left: auto; padding: 0; background: transparent; color: #999; font-size: 12px; font-weight: 400; box-shadow: none; }
.lovart-image-tool-cost > svg { width: 12px; height: 12px; }

View File

@ -25,8 +25,11 @@ import {
type UiSurfaceSnapshot,
} from "@/lib/ui-surface-protocol";
import useSWR, { useSWRConfig } from "swr";
import { Loader2, ImagePlus, X, Plus, History, LayoutGrid, ArrowRightToLine, RefreshCw, Globe, ArrowUp, Video, Sparkles, Check, ChevronDown, ChevronUp, AlertCircle, CheckCircle2, Brain, ThumbsUp, ThumbsDown, Eye, Trash2, Download, MessageCircle, Clapperboard, Bot, FileSpreadsheet, FileArchive, FolderUp, Paperclip, Copy, Square, Search, GripVertical, Volume2, LockKeyhole } from "lucide-react";
import { Loader2, ImagePlus, X, Plus, History, LayoutGrid, ArrowRightToLine, RefreshCw, Globe, ArrowUp, Video, Sparkles, Check, ChevronDown, ChevronUp, AlertCircle, CheckCircle2, Brain, ThumbsUp, ThumbsDown, Eye, Trash2, Download, MessageCircle, Clapperboard, Bot, FileSpreadsheet, FileArchive, FolderUp, Paperclip, Copy, Square, Search, GripVertical, Volume2, LockKeyhole, Images } from "lucide-react";
import { Button } from "@/components/ui/button";
import { CanvasComposerPromo } from "./CanvasComposerPromo";
import { ReferenceIcon } from "@/components/lovart/reference-icon";
import { ModelIdentityIcon } from "@/components/lovart/model-identity-icon";
import {
Collapsible,
CollapsibleContent,
@ -3334,6 +3337,7 @@ export function CanvasChatPanel({
const [isComposerModeMenuOpen, setIsComposerModeMenuOpen] = useState(false);
const [selectedSkillTagId, setSelectedSkillTagId] = useState<string | null>(null);
const [areCanvasSkillTagsExpanded, setAreCanvasSkillTagsExpanded] = useState(false);
const [isCanvasSkillPickerOpen, setIsCanvasSkillPickerOpen] = useState(false);
const [canvasSkillTagSearchQuery, setCanvasSkillTagSearchQuery] = useState("");
const [selectedCanvasSkillCategoryId, setSelectedCanvasSkillCategoryId] =
useState("all");
@ -14824,7 +14828,9 @@ export function CanvasChatPanel({
: selectedSkillTag?.placeholder?.trim() ||
(selectedSkillTag
? `请输入「${selectedSkillTag.name}」相关需求`
: "请输入你的设计需求");
: activeComposerMode === "image"
? "今天我们要创作什么"
: "请输入你的设计需求");
const clawdEditablePresetPlaceholder =
isClawdMode && activeClawdSkillTag
? activeClawdSkillTag.placeholder?.trim() || ""
@ -15312,10 +15318,14 @@ export function CanvasChatPanel({
"flex h-5 w-5 shrink-0 items-center justify-center overflow-hidden rounded-full leading-none",
isVideoWorkspace
? "border border-border/60 bg-background"
: "border border-transparent bg-white dark:border-white/20 dark:bg-white"
: "border border-transparent bg-white dark:border-white/20 dark:bg-white",
!isWorkspacePresentation && !isVideoWorkspace && "rounded-none border-0 bg-transparent dark:bg-transparent"
)}
>
{model.avatarUrl ? (
{!isWorkspacePresentation && !isVideoWorkspace ? (
<ModelIdentityIcon name={model.displayName} identity={`${model.id} ${model.providerType || ""}`} avatarUrl={model.avatarUrl ? buildPreviewImageUrl(model.avatarUrl, 40) : undefined} size={18} />
) : (
model.avatarUrl ? (
<ModelAvatarImage
src={buildPreviewImageUrl(model.avatarUrl, 40)}
alt={model.displayName}
@ -15342,6 +15352,7 @@ export function CanvasChatPanel({
>
{model.id === "auto" ? "A" : model.displayName.slice(0, 1)}
</span>
)
)}
</span>
<span className="min-w-0 flex-1 truncate text-[13px] font-normal leading-5">
@ -15496,6 +15507,7 @@ export function CanvasChatPanel({
return (
<div
data-canvas-chat-panel="ready"
data-lovart-chat={!isWorkspacePresentation && !isVideoWorkspace ? "true" : undefined}
className={cn(
"relative flex h-full min-h-0 flex-col",
isWorkspacePresentation
@ -15533,7 +15545,7 @@ export function CanvasChatPanel({
</div>
)}
{!isWorkspacePresentation ? (
<div className="z-20 flex items-center justify-between gap-2 px-3 py-2">
<div className="lovart-chat-header z-20 flex h-[40px] shrink-0 items-center justify-between gap-2 px-3 py-2">
<div className="min-w-0 flex flex-1 items-center gap-2 pr-2">
<span
className={cn(
@ -15983,25 +15995,18 @@ export function CanvasChatPanel({
) : null}
</div>
) : (
<div className="flex flex-1 flex-col px-5 pb-8 pt-8">
<div className="text-left">
<h1 className="text-[20px] font-semibold leading-tight text-[#050505] dark:text-zinc-50">
Hi,{" "}
{greetingUserName ? (
<span className="text-[#2563ff]">{greetingUserName}</span>
) : null}{" "}
{greetingPeriodText}
</h1>
<p className="mt-2 text-[13px] font-normal leading-5 text-[#6b7280] dark:text-zinc-400">
开始创作吧! 累计为你生成了{" "}
<span className="font-semibold text-[#2563ff]">
{Number.isFinite(totalGeneratedImages) ? totalGeneratedImages : 0}
</span>{" "}
张图。
</p>
<div className="lovart-chat-welcome flex min-h-0 flex-1 flex-col items-center justify-center overflow-y-auto px-5 py-6">
<div className="w-full max-w-[224px] text-center">
<h1 className="mb-7 text-[16px] font-semibold leading-6 text-[#222222] dark:text-zinc-50" title={`${greetingUserName} ${greetingPeriodText} · 已生成 ${Number.isFinite(totalGeneratedImages) ? totalGeneratedImages : 0} 张图`}>准备怎么开启你的创作?</h1>
<div className="flex flex-col items-center gap-3">
<button type="button" className="lovart-start-action" onClick={() => fileInputRef.current?.click()}><ReferenceIcon name="reference" size={16} className="text-[#a187ff]" />上传本地参考</button>
{canvasSkillTags.length > 0 ? <button type="button" className="lovart-start-action" aria-expanded={isCanvasSkillPickerOpen} onClick={() => setIsCanvasSkillPickerOpen((open) => !open)}><ReferenceIcon name="skill-filled" size={16} className="text-[#657dff]" />选择 Skill 开始创作</button> : null}
<button type="button" className="lovart-start-action" onClick={() => fileInputRef.current?.click()}><ReferenceIcon name="brand-filled" size={16} className="text-[#ee9b38]" />上传品牌素材</button>
{onOpenFileList ? <button type="button" className="lovart-start-action" onClick={onOpenFileList}><ReferenceIcon name="brand" size={16} className="text-[#727272]" />查看画布素材</button> : null}
</div>
</div>
{canvasSkillTags.length > 0 ? (
{canvasSkillTags.length > 0 && isCanvasSkillPickerOpen ? (
<div className="mt-6 rounded-[16px] border border-[#e4e7ec] bg-white px-4 py-4 text-center dark:border-white/10 dark:bg-zinc-950/70">
<h3 className="text-[14px] font-semibold leading-none text-[#1f2328] dark:text-zinc-100">
{canvasSkillTagsTitle}
@ -17077,37 +17082,53 @@ export function CanvasChatPanel({
<CanvasComposerUploadProgressDialog progress={composerUploadProgress} />
{!isWorkspacePresentation && shouldShowCanvasBanner ? (
<div className="px-[10px] pb-0">
<div
className={cn(
"relative z-10 mb-0 flex h-[48px] items-center justify-between gap-2 rounded-[28px] bg-[#f4f4f5] px-4 py-0 text-[14px] font-medium leading-5 text-[#20242a] dark:bg-zinc-900 dark:text-zinc-100",
siteSettings?.bannerLink && "cursor-pointer transition-colors hover:bg-[#eeeeef] dark:hover:bg-zinc-800"
)}
onClick={() => {
if (siteSettings?.bannerLink) {
window.open(siteSettings.bannerLink, "_blank");
}
}}
>
<div className="flex min-w-0 items-center gap-2.5">
<div className="flex h-7 w-7 shrink-0 items-center justify-center rounded-[9px] bg-[#e8ff38] text-[#111827]">
<Sparkles className="h-3.5 w-3.5 fill-current" />
</div>
<span className="min-w-0 truncate">{siteSettings?.bannerText || "限时特惠:升级会员最高立享 45% OFF!"}</span>
</div>
<button
type="button"
className="flex h-7 w-7 shrink-0 items-center justify-center rounded-full text-[#20242a]/75 transition-colors hover:bg-black/5 hover:text-[#20242a] dark:text-zinc-100/75 dark:hover:bg-white/10 dark:hover:text-zinc-100"
aria-label="关闭优惠广告"
onClick={(event) => {
event.stopPropagation();
setIsCanvasBannerDismissed(true);
!isVideoWorkspace ? (
<CanvasComposerPromo
text={siteSettings?.bannerText || "限时特惠:升级会员最高立享 45% OFF!"}
onDismiss={() => setIsCanvasBannerDismissed(true)}
onActivate={
siteSettings?.bannerLink
? () => {
if (siteSettings.bannerLink) {
window.open(siteSettings.bannerLink, "_blank");
}
}
: undefined
}
/>
) : (
<div className="lovart-chat-promo px-[8px] pb-0">
<div
className={cn(
"relative z-10 mb-0 flex h-[36px] items-center justify-between gap-2 rounded-t-[24px] bg-[#ededed] px-4 py-0 text-[12px] font-medium leading-5 text-[#20242a] dark:bg-zinc-900 dark:text-zinc-100",
siteSettings?.bannerLink && "cursor-pointer transition-colors hover:bg-[#eeeeef] dark:hover:bg-zinc-800"
)}
onClick={() => {
if (siteSettings?.bannerLink) {
window.open(siteSettings.bannerLink, "_blank");
}
}}
>
<X className="h-5 w-5" />
</button>
<div className="flex min-w-0 items-center gap-2.5">
<div className="flex h-7 w-7 shrink-0 items-center justify-center rounded-[9px] bg-[#e8ff38] text-[#111827]">
<Sparkles className="h-3.5 w-3.5 fill-current" />
</div>
<span className="min-w-0 truncate">{siteSettings?.bannerText || "限时特惠:升级会员最高立享 45% OFF!"}</span>
</div>
<button
type="button"
className="flex h-7 w-7 shrink-0 items-center justify-center rounded-full text-[#20242a]/75 transition-colors hover:bg-black/5 hover:text-[#20242a] dark:text-zinc-100/75 dark:hover:bg-white/10 dark:hover:text-zinc-100"
aria-label="关闭优惠广告"
onClick={(event) => {
event.stopPropagation();
setIsCanvasBannerDismissed(true);
}}
>
<X className="h-5 w-5" />
</button>
</div>
</div>
</div>
)
) : null}
{/* 底部输入区域 - 混合输入(图片标签+文字可自由混合) */}
@ -17115,15 +17136,16 @@ export function CanvasChatPanel({
className={cn(
isWorkspacePresentation
? "border-t bg-white/92 p-4 backdrop-blur-sm dark:bg-zinc-950/92"
: "bg-white px-2 pb-2 pt-0 dark:bg-zinc-950"
: "lovart-chat-composer bg-white px-[8px] pb-[8px] pt-0 dark:bg-zinc-950"
)}
>
<div
className={cn(
"relative flex flex-col overflow-hidden border border-[#e2e5ea] bg-white shadow-[0_18px_36px_-28px_rgba(15,23,42,0.42)] transition-[border-color,box-shadow] duration-150 focus-within:border-[#cfd5de] focus-within:shadow-[0_20px_40px_-28px_rgba(15,23,42,0.5)] dark:border-white/12 dark:bg-zinc-950 dark:focus-within:border-white/25",
"min-h-[148px] rounded-[18px]"
"min-h-[120px] rounded-[24px]"
)}
data-canvas-chat-input-anchor="true"
data-canvas-composer-mode={activeComposerMode}
>
{isWorkspacePresentation && !isDetailGenerationMode ? (
<div className="border-b px-5 py-3">
@ -17879,7 +17901,10 @@ export function CanvasChatPanel({
{/* 混合输入区域:使用 contenteditable 实现真正的混合编辑 */}
<div
className={cn(
"relative px-3 pb-0 pt-2.5",
"relative pb-0",
!isWorkspacePresentation && !isDetailGenerationMode && !isVideoWorkspace && activeComposerMode === "image"
? "px-0 pt-0"
: "px-3 pt-2.5",
isDetailGenerationMode
? "min-h-[96px]"
: activeComposerMode === "image"
@ -18258,16 +18283,17 @@ export function CanvasChatPanel({
</div>
) : null}
{!isWorkspacePresentation && !isDetailGenerationMode && !isVideoWorkspace && activeComposerMode === "image" ? (
<div className="mx-4 mt-3 flex items-center">
<div className="lovart-reference-upload mx-[8px] mt-[8px] flex items-center">
<button
type="button"
className="relative flex h-8 w-8 shrink-0 items-center justify-center overflow-hidden rounded-full border border-dashed border-[#d9dde4] bg-[#f3f3f4] text-[#8f97a3] transition-colors hover:border-[#c6ccd6] hover:bg-[#ececee] hover:text-[#20242a] disabled:cursor-not-allowed disabled:opacity-60 dark:border-white/12 dark:bg-white/8 dark:text-zinc-400 dark:hover:border-white/20 dark:hover:bg-white/12 dark:hover:text-white"
className="lovart-reference-tile relative flex h-8 w-8 shrink-0 items-center justify-center overflow-hidden rounded-full border border-dashed border-[#d9dde4] bg-[#f3f3f4] text-[#8f97a3] transition-colors hover:border-[#c6ccd6] hover:bg-[#ececee] hover:text-[#20242a] disabled:cursor-not-allowed disabled:opacity-60 dark:border-white/12 dark:bg-white/8 dark:text-zinc-400 dark:hover:border-white/20 dark:hover:bg-white/12 dark:hover:text-white"
onClick={() => fileInputRef.current?.click()}
disabled={isSubmitting}
title={supportsAtlasVideoReference ? "添加图片或参考视频" : "添加图片"}
aria-label={supportsAtlasVideoReference ? "添加图片或参考视频" : "添加图片"}
>
<Plus className="h-4 w-4" />
<ReferenceIcon name="reference-image" size={16} />
<span>参考图</span>
{composerReferenceUploadProgress ? (
<div className="absolute inset-0 flex items-center justify-center bg-black/70 text-white backdrop-blur-[1px]">
<span className="text-[10px] font-semibold leading-none tabular-nums">
@ -18329,6 +18355,7 @@ export function CanvasChatPanel({
) : null}
<div
className={cn(
"lovart-composer-text-region",
isWorkspacePresentation || isDetailGenerationMode || isVideoWorkspace
? "px-1"
: "px-2 pt-2"
@ -18337,6 +18364,11 @@ export function CanvasChatPanel({
<div
ref={textareaRef as any}
contentEditable={!isSubmitting}
role="textbox"
tabIndex={isSubmitting ? -1 : 0}
aria-disabled={isSubmitting}
aria-label="描述你的设计需求"
aria-multiline="true"
suppressContentEditableWarning
className={cn(
"max-h-[128px] overflow-y-auto outline-none empty:before:content-[attr(data-placeholder)]",
@ -18564,7 +18596,7 @@ export function CanvasChatPanel({
{/* Bottom Toolbar */}
<div
className={cn(
"mt-auto flex min-h-10 items-center justify-between gap-1.5 px-2.5 pb-2 pt-1 dark:bg-zinc-950"
"lovart-composer-footer mt-auto flex min-h-10 items-center justify-between gap-1.5 px-2.5 pb-2 pt-1 dark:bg-zinc-950"
)}
>
<div className="flex min-w-0 flex-1 items-center gap-1 overflow-x-auto pr-1 scrollbar-hide">
@ -18698,7 +18730,7 @@ export function CanvasChatPanel({
type="button"
variant="ghost"
className={cn(
"relative h-8 max-w-[110px] shrink-0 cursor-pointer rounded-full px-2.5 text-[13px] font-medium outline-none focus:outline-none focus:ring-0 focus:ring-offset-0 focus-visible:outline-none focus-visible:ring-0 focus-visible:ring-offset-0",
"lovart-composer-mode-selector relative h-8 max-w-[110px] shrink-0 cursor-pointer rounded-full px-2.5 text-[13px] font-medium outline-none focus:outline-none focus:ring-0 focus:ring-offset-0 focus-visible:outline-none focus-visible:ring-0 focus-visible:ring-offset-0",
isVideoToolbarDark
? "bg-[#242529] text-zinc-100 hover:bg-[#2c2d33]"
: "bg-[#eeeeef] text-[#20242a] hover:bg-[#e7e7e8] dark:bg-white/10 dark:text-zinc-100 dark:hover:bg-white/14"
@ -18724,6 +18756,8 @@ export function CanvasChatPanel({
className="mr-1 h-4 w-4 shrink-0"
fallback={ImagePlus}
item={getToolbarItem("chatModeImageActive")}
referenceName={!isWorkspacePresentation && !isVideoWorkspace ? "image" : undefined}
referenceSize={20}
/>
) : (
<CanvasToolbarIcon
@ -18733,7 +18767,9 @@ export function CanvasChatPanel({
/>
)}
<span className="min-w-0 truncate">{activeModeLabel}</span>
{isComposerModeMenuOpen ? (
{!isWorkspacePresentation && !isVideoWorkspace ? (
<ReferenceIcon name="chevron-down" size={18} className={cn("ml-1.5", isComposerModeMenuOpen && "rotate-180")} />
) : isComposerModeMenuOpen ? (
<ChevronUp className="ml-1.5 h-3.5 w-3.5 shrink-0" />
) : (
<ChevronDown className="ml-1.5 h-3.5 w-3.5 shrink-0" />
@ -18865,6 +18901,7 @@ export function CanvasChatPanel({
hideImageCount={false}
hideAdvancedOptions={true}
inlineTheme="minimal"
referencePresentation={!isWorkspacePresentation && !isVideoWorkspace}
showInlineDivider={false}
/>
) : null}
@ -18895,6 +18932,7 @@ export function CanvasChatPanel({
>
<SelectTrigger
className={cn(
cn(!isVideoWorkspace && activeComposerMode === "image" && "lovart-model-selector"),
"h-8 w-8 shrink-0 justify-center rounded-full p-0 outline-none focus:outline-none focus:ring-0 focus:ring-offset-0 focus-visible:outline-none focus-visible:ring-0 focus-visible:ring-offset-0 [&>svg]:hidden",
isVideoToolbarDark
? "border border-white/12 bg-[#242529] text-zinc-100 hover:bg-[#2c2d33]"
@ -18902,6 +18940,13 @@ export function CanvasChatPanel({
)}
title={getToolbarItem("chatModelSelector").label}
>
{!isVideoWorkspace && activeComposerMode === "image" ? (
<>
<ModelIdentityIcon name={selectDisplayModel?.displayName || "自动模型"} identity={`${selectDisplayModel?.id || ""} ${selectDisplayModel?.providerType || ""}`} avatarUrl={selectDisplayModel?.avatarUrl ? buildPreviewImageUrl(selectDisplayModel.avatarUrl, 64) : undefined} size={18} />
<span className="min-w-0 flex-1 truncate">{selectDisplayModel?.displayName || "自动模型"}</span>
<ReferenceIcon name="chevron-down" size={18} />
</>
) : (
<span
className={cn(
"flex h-5 w-5 shrink-0 items-center justify-center overflow-hidden rounded-full leading-none ring-1",
@ -18946,12 +18991,13 @@ export function CanvasChatPanel({
</span>
)}
</span>
)}
</SelectTrigger>
{renderModelSelectContent()}
</Select>
) : null}
{estimatedPointsCost ? (
{estimatedPointsCost && (isWorkspacePresentation || isVideoWorkspace || activeComposerMode !== "image") ? (
<div
className={cn(
"ml-0 flex h-7 max-w-[60px] shrink items-center gap-1 rounded-full px-2 text-[10px] sm:ml-1 sm:max-w-none sm:gap-1 sm:px-2 sm:text-[11px]",
@ -18976,6 +19022,7 @@ export function CanvasChatPanel({
<Button
size={isWorkspacePresentation ? "default" : "icon"}
className={cn(
"lovart-composer-send",
isWorkspacePresentation
? "h-9 rounded-full px-3.5 transition-all duration-150 flex-shrink-0"
: "h-8 w-8 rounded-full transition-all duration-150 flex-shrink-0",
@ -19006,6 +19053,8 @@ export function CanvasChatPanel({
? "停止处理并重新编辑"
: isWorkspacePresentation || isDetailGenerationMode
? getToolbarItem("chatStartGeneration").label
: !isVideoWorkspace && activeComposerMode === "image" && estimatedPointsCost
? `${getToolbarItem("chatSend").label} · ${estimatedPointsCost.title}`
: getToolbarItem("chatSend").label
}
>
@ -19039,6 +19088,11 @@ export function CanvasChatPanel({
</span>
) : null}
</>
) : !isWorkspacePresentation && !isVideoWorkspace && activeComposerMode === "image" && estimatedPointsCost ? (
<>
<CanvasToolbarIcon fallback={Sparkles} item={getToolbarItem("chatPoints")} referenceName="points" referenceSize={14} />
<span>{estimatedPointsCost.cost}</span>
</>
) : (
<>
<CanvasToolbarIcon

View File

@ -0,0 +1,54 @@
"use client";
import { ReferenceIcon } from "@/components/lovart/reference-icon";
type CanvasComposerPromoProps = {
text: string;
onDismiss: () => void;
onActivate?: () => void;
};
export function CanvasComposerPromo({
text,
onDismiss,
onActivate,
}: CanvasComposerPromoProps) {
return (
<div className="lovart-chat-promo lovart-chat-promo-connected">
<div className="lovart-chat-promo-surface">
<div className="lovart-chat-promo-row">
<button
type="button"
className="lovart-chat-promo-message"
onClick={onActivate}
disabled={!onActivate}
title={text}
>
<svg
aria-hidden="true"
width="16"
height="18"
viewBox="0 0 24 28"
className="lovart-chat-promo-badge"
>
<path d="M10 1.15a4 4 0 0 1 4 0l8 4.6a4 4 0 0 1 2 3.46v9.58a4 4 0 0 1-2 3.46l-8 4.6a4 4 0 0 1-4 0l-8-4.6A4 4 0 0 1 0 18.79V9.21a4 4 0 0 1 2-3.46Z" fill="#ddef65" />
<path
d="M12 5.5c1.2 5.3 2.2 6.3 7.5 7.5-5.3 1.2-6.3 2.2-7.5 7.5C10.8 15.2 9.8 14.2 4.5 13c5.3-1.2 6.3-2.2 7.5-7.5Z"
fill="#111"
/>
</svg>
<span>{text}</span>
</button>
<button
type="button"
className="lovart-chat-promo-close"
aria-label="关闭优惠广告"
onClick={onDismiss}
>
<ReferenceIcon name="close" size={18} />
</button>
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,99 @@
"use client";
import { ReferenceIcon } from "@/components/lovart/reference-icon";
import { Loader2, Save } from "lucide-react";
type CanvasSurfaceHeaderProps = {
isSaving: boolean;
onAddImage: () => void;
onHome: () => void;
onOpenFiles: () => void;
onSave: () => void;
projectTitle: string;
rightInset: number;
};
const iconButtonClass =
"inline-flex h-[28px] w-[28px] items-center justify-center rounded-[8px] p-0 text-[#858585] transition-colors hover:bg-black/5 focus-visible:outline-2 focus-visible:outline-offset-2 dark:hover:bg-white/10";
export function CanvasSurfaceHeader({
isSaving,
onAddImage,
onHome,
onOpenFiles,
onSave,
projectTitle,
rightInset,
}: CanvasSurfaceHeaderProps) {
return (
<>
<div className="absolute inset-x-0 top-0 z-30 flex h-[40px] items-center border-b border-[#ededed] bg-white px-3 dark:border-white/10 dark:bg-zinc-950">
<button
aria-label="返回首页"
className={iconButtonClass}
onClick={onHome}
title="返回首页"
type="button"
>
<ReferenceIcon name="branding" size={16} />
</button>
</div>
<div
className="absolute left-0 top-[40px] z-20 flex h-[40px] items-center justify-between border-b border-[#ededed] bg-white px-3 dark:border-white/10 dark:bg-zinc-950"
data-canvas-topbar="true"
style={{ right: rightInset }}
>
<div className="flex min-w-0 items-center gap-2">
<button
aria-label="添加参考素材"
className={iconButtonClass}
onClick={onAddImage}
title="添加参考素材"
type="button"
>
<ReferenceIcon name="reference" size={16} />
</button>
<button
aria-label="查看项目文件"
className={iconButtonClass}
onClick={onOpenFiles}
title="查看项目文件"
type="button"
>
<ReferenceIcon name="brand" size={16} />
</button>
<div className="mx-1 h-4 w-px bg-[#ededed] dark:bg-white/10" />
<span className="inline-flex h-7 min-w-0 items-center gap-2 rounded-[8px] bg-[#ededed] px-3 text-[13px] text-[#333333] dark:bg-white/10 dark:text-white">
<ReferenceIcon name="page" size={16} />
<span className="max-w-[160px] truncate">
{projectTitle || "页面 1"}
</span>
</span>
<button
aria-label="添加到画布"
className={iconButtonClass}
onClick={onAddImage}
title="添加到画布"
type="button"
>
<ReferenceIcon name="plus" size={16} />
</button>
</div>
<button
className="inline-flex h-7 shrink-0 items-center gap-1.5 rounded-[8px] border border-[#e5e5e5] px-3 text-[12px] text-[#333333] transition-colors hover:bg-black/5 disabled:opacity-50 dark:border-white/10 dark:text-white"
disabled={isSaving}
onClick={onSave}
type="button"
>
{isSaving ? (
<Loader2 className="h-3.5 w-3.5 animate-spin" />
) : (
<Save className="h-3.5 w-3.5" />
)}
保存
</button>
</div>
</>
);
}

View File

@ -1,6 +1,10 @@
"use client";
import type { CSSProperties } from "react";
import {
ReferenceIcon,
type ReferenceIconName,
} from "@/components/lovart/reference-icon";
import type { LucideIcon } from "lucide-react";
import type { CanvasToolbarItemConfig } from "@/lib/canvas-toolbar-config";
import { cn } from "@/lib/utils";
@ -12,13 +16,12 @@ type CanvasToolbarIconProps = {
className?: string;
style?: CSSProperties;
active?: boolean;
referenceName?: ReferenceIconName;
referenceSize?: number;
};
function isSvgIconUrl(url: string) {
return (
/^data:image\/svg\+xml/i.test(url) ||
/\.svg(?:[?#].*)?$/i.test(url)
);
return /^data:image\/svg\+xml/i.test(url) || /\.svg(?:[?#].*)?$/i.test(url);
}
function stripImageFilterClasses(className?: string) {
@ -42,6 +45,8 @@ export function CanvasToolbarIcon({
className,
style,
active = false,
referenceName,
referenceSize = 18,
}: CanvasToolbarIconProps) {
const resolvedIconUrl = String(iconUrl || item?.iconUrl || "").trim();
@ -75,7 +80,9 @@ export function CanvasToolbarIcon({
aria-hidden="true"
className={cn(
"object-contain",
active ? "brightness-0 invert opacity-95" : "brightness-0 opacity-80 dark:invert",
active
? "brightness-0 invert opacity-95"
: "brightness-0 opacity-80 dark:invert",
className
)}
src={resolvedIconUrl}
@ -84,5 +91,15 @@ export function CanvasToolbarIcon({
);
}
if (referenceName) {
return (
<ReferenceIcon
name={referenceName}
size={referenceSize}
className={className}
style={style}
/>
);
}
return <FallbackIcon className={className} style={style} />;
}

View File

@ -0,0 +1,452 @@
"use client";
import { ChevronRight, X } from "lucide-react";
import {
Children,
cloneElement,
forwardRef,
isValidElement,
useState,
type HTMLAttributes,
type MouseEvent,
type ReactElement,
type ReactNode,
} from "react";
import { toast } from "sonner";
import { ReferenceIcon } from "@/components/lovart/reference-icon";
import { CanvasToolbarIcon } from "@/components/canvas/CanvasToolbarIcon";
import {
Dialog,
DialogContent,
DialogDescription,
DialogTitle,
} from "@/components/ui/dialog";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip";
import {
saveImageToolbarPreferences,
useImageToolbarPreferences,
type ImageToolbarPreferences,
} from "@/lib/canvas-image-toolbar-preferences";
type ToolProps = HTMLAttributes<HTMLButtonElement> & {
"data-tool-key"?: string;
disabled?: boolean;
children?: ReactNode;
};
type Tool = ReactElement<ToolProps>;
type Props = HTMLAttributes<HTMLDivElement> & {
referencePresentation: boolean;
defaultPinnedKeys: string[];
};
function toolLabel(tool: Tool) {
return tool.props.title || "";
}
function toolIcon(tool: Tool, size: number) {
const icon = Children.toArray(tool.props.children).find(isValidElement);
if (
!isValidElement<{
className?: string;
style?: object;
referenceSize?: number;
}>(icon)
)
return null;
return cloneElement(icon, {
className: "shrink-0",
style: { width: size, height: size },
...(icon.type === CanvasToolbarIcon ? { referenceSize: size } : {}),
});
}
function toolCredit(tool: Tool) {
return Children.toArray(tool.props.children).find(
(child) =>
isValidElement<{ compact?: boolean; label?: string }>(child) &&
child.props.compact &&
child.props.label
) as
| ReactElement<{ compact?: boolean; label?: string; className?: string }>
| undefined;
}
function tooltipButton(tool: Tool, showNames: boolean) {
const name = toolLabel(tool);
// Clone the existing action so its handler, busy state and administrator icon override remain intact.
const button = cloneElement(tool, {
title: undefined,
"aria-label": name,
children: (
<>
{toolIcon(tool, 25)}
{showNames && <span className="lovart-image-tool-label">{name}</span>}
</>
),
});
return (
<Tooltip key={tool.props["data-tool-key"] || name}>
<TooltipTrigger asChild>{button}</TooltipTrigger>
<TooltipContent
className="lovart-image-tool-tooltip"
side="top"
sideOffset={8}
>
{name}
{toolCredit(tool) &&
cloneElement(toolCredit(tool)!, {
compact: false,
className: "lovart-image-tool-cost",
})}
</TooltipContent>
</Tooltip>
);
}
export const ReferenceImageToolbar = forwardRef<HTMLDivElement, Props>(
function ReferenceImageToolbar(
{ referencePresentation, defaultPinnedKeys, children, ...rootProps },
ref
) {
const saved = useImageToolbarPreferences();
const [draft, setDraft] = useState<ImageToolbarPreferences | null>(null);
const [draggedKey, setDraggedKey] = useState<string | null>(null);
const elements = Children.toArray(children).filter(
isValidElement
) as ReactElement<ToolProps>[];
const tools = elements.filter(
(element) =>
element.props["data-tool-key"] &&
!["addToChat", "download"].includes(element.props["data-tool-key"]!)
);
const toolMap = new Map(
tools.map((tool) => [tool.props["data-tool-key"]!, tool])
);
const preferences = saved || {
pinnedKeys: defaultPinnedKeys,
showNames: false,
};
const pinned = preferences.pinnedKeys.filter((key) => toolMap.has(key));
const moreTools = tools.filter(
(tool) => !pinned.includes(tool.props["data-tool-key"]!)
);
const addToChat = elements.find(
(element) => element.props["data-tool-key"] === "addToChat"
);
const download = elements.find(
(element) => element.props["data-tool-key"] === "download"
);
const originalMenu = elements.find(
(element) => element.type === DropdownMenu
);
const menuChildren = originalMenu
? (Children.toArray(originalMenu.props.children).filter(
isValidElement
) as ReactElement<{ children?: ReactNode }>[])
: [];
const menuTrigger = menuChildren.find(
(element) => element.type === DropdownMenuTrigger
);
const moreButton =
menuTrigger &&
(Children.toArray(menuTrigger.props.children).find(isValidElement) as
| Tool
| undefined);
const extras = menuChildren.find(
(element) => element.type === DropdownMenuContent
)?.props.children;
const openCustomizer = () =>
setDraft({
pinnedKeys: [...preferences.pinnedKeys],
showNames: preferences.showNames,
});
const toggle = (key: string) =>
setDraft((current) =>
current
? {
...current,
pinnedKeys: current.pinnedKeys.includes(key)
? current.pinnedKeys.filter((item) => item !== key)
: [...current.pinnedKeys, key],
}
: current
);
const move = (key: string, target: string) =>
setDraft((current) => {
if (!current || key === target) return current;
const next = [...current.pinnedKeys];
const from = next.indexOf(key),
to = next.indexOf(target);
if (from < 0 || to < 0) return current;
next.splice(from, 1);
next.splice(to, 0, key);
return { ...current, pinnedKeys: next };
});
if (!referencePresentation)
return (
<div {...rootProps} ref={ref}>
{children}
</div>
);
return (
<>
<div
{...rootProps}
ref={ref}
style={{ ...rootProps.style, overflowX: "auto" }}
data-show-tool-names={preferences.showNames ? "true" : "false"}
>
{addToChat && tooltipButton(addToChat, preferences.showNames)}
{addToChat && <div className="w-px" />}
{pinned.map((key) =>
tooltipButton(toolMap.get(key)!, preferences.showNames)
)}
<DropdownMenu>
{moreButton && (
<Tooltip>
<TooltipTrigger asChild>
<DropdownMenuTrigger asChild>
{cloneElement(moreButton, {
title: undefined,
"aria-label": toolLabel(moreButton),
children: toolIcon(moreButton, 25),
})}
</DropdownMenuTrigger>
</TooltipTrigger>
<TooltipContent
className="lovart-image-tool-tooltip"
sideOffset={8}
>
{toolLabel(moreButton)}
</TooltipContent>
</Tooltip>
)}
<DropdownMenuContent
className="lovart-image-action-menu lovart-image-custom-more"
side="bottom"
align="end"
>
{moreTools.map((tool) => (
<DropdownMenuItem
key={tool.props["data-tool-key"]}
disabled={tool.props.disabled}
onClick={(event) =>
tool.props.onClick?.(
event as unknown as MouseEvent<HTMLButtonElement>
)
}
>
{toolIcon(tool, 18)}
<span>{toolLabel(tool)}</span>
{toolCredit(tool) &&
cloneElement(toolCredit(tool)!, {
compact: false,
className: "lovart-image-tool-cost",
})}
</DropdownMenuItem>
))}
{extras}
<DropdownMenuSeparator />
<DropdownMenuItem
className="lovart-customize-entry"
onSelect={openCustomizer}
>
自定义工具栏
<ChevronRight size={14} />
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
{download && (
<>
<div className="w-px" />
{tooltipButton(download, preferences.showNames)}
</>
)}
</div>
<Dialog
open={Boolean(draft)}
onOpenChange={(open) => {
if (!open) setDraft(null);
}}
>
<DialogContent
hideCloseButton
className="lovart-toolbar-customizer"
overlayClassName="lovart-toolbar-customizer-overlay"
onPointerDown={(event) => event.stopPropagation()}
onClick={(event) => event.stopPropagation()}
>
<header className="lovart-customizer-header">
<div>
<DialogTitle>自定义工具栏</DialogTitle>
<DialogDescription>
选择你想在编辑栏中使用的工具
</DialogDescription>
</div>
<div className="lovart-customizer-header-actions">
<button
type="button"
aria-label="恢复默认工具栏"
onClick={() =>
setDraft({
pinnedKeys: [...defaultPinnedKeys],
showNames: false,
})
}
>
<ReferenceIcon name="toolbar-reset" size={16} />
</button>
<button
type="button"
aria-label="关闭自定义工具栏"
onClick={() => setDraft(null)}
>
<ReferenceIcon name="toolbar-close" size={16} />
</button>
</div>
</header>
<div className="lovart-customizer-body">
<div
className="lovart-customizer-preview"
aria-label="工具栏预览"
>
<div className="lovart-customizer-preview-bar">
{addToChat && (
<span className="lovart-preview-fixed">
{toolIcon(addToChat, 18)}
{draft?.showNames && toolLabel(addToChat)}
</span>
)}
{draft?.pinnedKeys
.filter((key) => toolMap.has(key))
.map((key, index, keys) => {
const tool = toolMap.get(key)!;
return (
<div
className="lovart-preview-chip"
key={key}
draggable
onDragStart={(event) => {
setDraggedKey(key);
event.dataTransfer.setData("text/plain", key);
event.dataTransfer.effectAllowed = "move";
}}
onDragEnd={() => setDraggedKey(null)}
onDragOver={(event) => event.preventDefault()}
onDrop={(event) => {
event.preventDefault();
if (draggedKey) move(draggedKey, key);
setDraggedKey(null);
}}
>
<button
type="button"
className="lovart-preview-handle"
aria-label={`${toolLabel(tool)},按左右方向键调整顺序`}
onKeyDown={(event) => {
const offset =
event.key === "ArrowLeft"
? -1
: event.key === "ArrowRight"
? 1
: 0;
if (offset && keys[index + offset]) {
event.preventDefault();
move(key, keys[index + offset]);
}
}}
>
{toolIcon(tool, 18)}
{draft.showNames && <span>{toolLabel(tool)}</span>}
</button>
<button
type="button"
aria-label={`移除${toolLabel(tool)}`}
onClick={() => toggle(key)}
>
<X size={12} />
</button>
</div>
);
})}
</div>
</div>
<div className="lovart-customizer-tools">
{tools.map((tool) => {
const key = tool.props["data-tool-key"]!;
const isPinned = Boolean(draft?.pinnedKeys.includes(key));
return (
<button
key={key}
type="button"
aria-label={`${isPinned ? "取消固定" : "固定"}${toolLabel(tool)}`}
aria-pressed={isPinned}
onClick={() => toggle(key)}
>
{toolIcon(tool, 18)}
<span>{toolLabel(tool)}</span>
<ReferenceIcon
name={
isPinned
? "toolbar-pin-filled"
: "toolbar-pin-outline"
}
size={13}
/>
</button>
);
})}
</div>
</div>
<footer className="lovart-customizer-footer">
<label>
<button
type="button"
role="switch"
aria-label="显示工具名称"
aria-checked={Boolean(draft?.showNames)}
className="lovart-customizer-switch"
onClick={() =>
setDraft((current) =>
current
? { ...current, showNames: !current.showNames }
: current
)
}
>
<span />
</button>
显示工具名称
</label>
<div>
<button type="button" onClick={() => setDraft(null)}>
取消
</button>
<button
type="button"
className="lovart-customizer-save"
onClick={() => {
if (draft && saveImageToolbarPreferences(draft))
setDraft(null);
else
toast.error("无法保存工具栏设置,请检查浏览器存储权限");
}}
>
保存
</button>
</div>
</footer>
</DialogContent>
</Dialog>
</>
);
}
);

View File

@ -42,7 +42,7 @@ import {
Zap,
} from "lucide-react";
import type React from "react";
import { useParams, useRouter } from "next/navigation";
import { useParams, usePathname, useRouter } from "next/navigation";
import { Fragment, useCallback, useEffect, useMemo, useRef, useState } from "react";
import { createPortal } from "react-dom";
import { toast } from "sonner";
@ -56,6 +56,7 @@ import { CanvasLayoutReplaceBatchDialog } from "@/components/canvas/CanvasLayout
import { TextEditColorField } from "@/components/canvas/TextEditColorField";
import { DetailSurfaceOverlayRenderer } from "@/components/canvas/DetailSurfaceOverlayRenderer";
import { DetailUiDomRenderer } from "@/components/canvas/DetailUiDomRenderer";
import { ReferenceImageToolbar } from "@/components/canvas/ReferenceImageToolbar";
import { CanvasToolbarIcon } from "@/components/canvas/CanvasToolbarIcon";
import { CanvasImageDownloadProgressDialog } from "@/components/canvas/CanvasImageDownloadProgressDialog";
import {
@ -1307,6 +1308,8 @@ export function ImageNode({
}: ImageNodeProps) {
const router = useRouter();
const routeParams = useParams();
const pathname = usePathname();
const useReferenceImageToolbar = /^\/canvas\/[^/]+\/?$/.test(pathname);
const canvasId = Array.isArray(routeParams?.id)
? routeParams.id[0] || ""
: typeof routeParams?.id === "string"
@ -1532,9 +1535,11 @@ export function ImageNode({
.map((action) => action.key)
);
const isSelectedImageActionInline = (key: SelectedImageToolbarActionKey) =>
selectedImageInlineActionKeys.has(key);
useReferenceImageToolbar
? enabledSelectedImageToolbarActions.some((action) => action.key === key)
: selectedImageInlineActionKeys.has(key);
const isSelectedImageActionInMore = (key: SelectedImageToolbarActionKey) =>
selectedImageMoreActionKeys.has(key);
!useReferenceImageToolbar && selectedImageMoreActionKeys.has(key);
const hasHoverImageMorePrimaryActions =
imageRemoveBackgroundEnabled ||
imageRemoveWatermarkEnabled ||
@ -1560,6 +1565,7 @@ export function ImageNode({
const hasSelectedImageMorePrimaryActions =
selectedImageMoreActionKeys.size > 0;
const hasSelectedImageMoreMenu =
useReferenceImageToolbar ||
hasSelectedImageMorePrimaryActions ||
(imageNodeMoreEnabled && hasCanvasAgentAnnotations) ||
(imageNodeMoreEnabled && imageDeleteEnabled);
@ -10192,8 +10198,12 @@ export function ImageNode({
typeof document !== "undefined" &&
createPortal(
<TooltipProvider delayDuration={200}>
<div
className="fixed z-[9999] flex items-center gap-0.5 rounded-[24px] border border-white/70 bg-white/82 px-2 py-1.5 text-gray-800 shadow-[0_18px_52px_rgba(15,23,42,0.20)] backdrop-blur-2xl before:pointer-events-none before:absolute before:inset-[-10px] before:-z-10 before:rounded-[32px] before:bg-white/35 before:shadow-[0_18px_64px_rgba(15,23,42,0.18)] before:backdrop-blur-2xl before:content-[''] supports-[backdrop-filter]:bg-white/72 dark:border-white/12 dark:bg-[#08090c]/72 dark:text-zinc-100 dark:shadow-[0_24px_72px_rgba(0,0,0,0.58)] dark:before:bg-black/35 dark:before:shadow-[0_24px_80px_rgba(0,0,0,0.62)] supports-[backdrop-filter]:dark:bg-[#08090c]/62"
<ReferenceImageToolbar
referencePresentation={useReferenceImageToolbar}
defaultPinnedKeys={enabledSelectedImageToolbarActions.slice(0, selectedImageInlineActionCount).map((action) => action.key)}
className={cn("fixed z-[9999] flex items-center gap-0.5 rounded-[24px] border border-white/70 bg-white/82 px-2 py-1.5 text-gray-800 shadow-[0_18px_52px_rgba(15,23,42,0.20)] backdrop-blur-2xl before:pointer-events-none before:absolute before:inset-[-10px] before:-z-10 before:rounded-[32px] before:bg-white/35 before:shadow-[0_18px_64px_rgba(15,23,42,0.18)] before:backdrop-blur-2xl before:content-[''] supports-[backdrop-filter]:bg-white/72 dark:border-white/12 dark:bg-[#08090c]/72 dark:text-zinc-100 dark:shadow-[0_24px_72px_rgba(0,0,0,0.58)] dark:before:bg-black/35 dark:before:shadow-[0_24px_80px_rgba(0,0,0,0.62)] supports-[backdrop-filter]:dark:bg-[#08090c]/62", useReferenceImageToolbar && "lovart-image-action-bar")}
data-selected-image-toolbar="true"
data-reference-presentation={useReferenceImageToolbar ? "true" : undefined}
onClick={(e) => e.stopPropagation()}
onMouseDown={(e) => e.stopPropagation()}
onPointerDown={(e) => e.stopPropagation()}
@ -10217,7 +10227,8 @@ export function ImageNode({
className="mr-1 inline-flex h-10 shrink-0 items-center gap-1.5 whitespace-nowrap rounded-xl bg-slate-900 px-3 font-semibold text-[13px] text-white hover:bg-slate-800 hover:text-white dark:bg-slate-100 dark:text-slate-900 dark:hover:bg-slate-200"
disabled={isProcessing || isAddingToChat}
onClick={handleAddToChat}
title={imageAddToChatItem.label}
data-tool-key="addToChat"
title={imageAddToChatItem.label}
variant="ghost"
>
{isAddingToChat ? (
@ -10228,6 +10239,8 @@ export function ImageNode({
className="h-4 w-4 dark:invert-0"
fallback={MessageSquarePlus}
item={imageAddToChatItem}
referenceName={useReferenceImageToolbar ? "image-mark" : undefined}
referenceSize={25}
/>
)}
<span className="whitespace-nowrap">{imageAddToChatItem.label}</span>
@ -10242,6 +10255,7 @@ export function ImageNode({
{isSelectedImageActionInline("pickObject") ? (
<Button
data-tool-key="pickObject"
className="h-10 rounded-xl px-3 font-medium text-[13px] text-slate-700 hover:bg-slate-100 hover:text-slate-900 dark:text-slate-100 dark:hover:bg-zinc-800"
disabled={isProcessing || isPickerLoading || isThisNodePicking}
onClick={handleStartPicking}
@ -10252,6 +10266,8 @@ export function ImageNode({
className="mr-1.5 h-4 w-4"
fallback={Crosshair}
item={imagePickObjectItem}
referenceName={useReferenceImageToolbar ? "image-move-object" : undefined}
referenceSize={25}
/>
{imagePickObjectItem.label}
</Button>
@ -10259,6 +10275,7 @@ export function ImageNode({
{isSelectedImageActionInline("enhance") ? (
<Button
data-tool-key="enhance"
className="group relative h-10 rounded-xl px-3 font-medium text-[13px] text-slate-700 hover:bg-slate-100 hover:text-slate-900 dark:text-slate-100 dark:hover:bg-zinc-800"
disabled={isProcessing}
onClick={() => handleAction("enhance")}
@ -10269,6 +10286,8 @@ export function ImageNode({
className="mr-1.5 h-4 w-4"
fallback={Sparkles}
item={imageEnhanceItem}
referenceName={useReferenceImageToolbar ? "image-upscale" : undefined}
referenceSize={25}
/>
{imageEnhanceItem.label}
<ImageActionCreditBadge compact label={enhanceCostShortLabel} />
@ -10277,6 +10296,7 @@ export function ImageNode({
{isSelectedImageActionInline("removeBackground") ? (
<Button
data-tool-key="removeBackground"
className="group relative h-10 rounded-xl px-3 font-medium text-[13px] text-slate-700 hover:bg-slate-100 hover:text-slate-900 dark:text-slate-100 dark:hover:bg-zinc-800"
disabled={isProcessing}
onClick={() => handleAction("rmbg")}
@ -10287,6 +10307,8 @@ export function ImageNode({
className="mr-1.5 h-4 w-4"
fallback={Scissors}
item={imageRemoveBackgroundItem}
referenceName={useReferenceImageToolbar ? "image-remove-background" : undefined}
referenceSize={25}
/>
{imageRemoveBackgroundItem.label}
<ImageActionCreditBadge
@ -10298,6 +10320,7 @@ export function ImageNode({
{isSelectedImageActionInline("aiWashQuick") ? (
<Button
data-tool-key="aiWashQuick"
className={cn(
"group relative h-10 rounded-xl px-3 font-medium text-[13px] transition",
aiWashQuickPanelOpen
@ -10307,6 +10330,7 @@ export function ImageNode({
disabled={isProcessing || isSubmittingAiWashQuick}
onClick={() => handleAction("ai-wash")}
title={aiWashQuickModeLabel}
data-reference-active={aiWashQuickPanelOpen ? "true" : undefined}
variant="ghost"
>
{isSubmittingAiWashQuick ? (
@ -10317,6 +10341,8 @@ export function ImageNode({
className="mr-1.5 h-4 w-4"
fallback={Sparkles}
iconUrl={aiWashQuickConfig.modeIconUrl}
referenceName={useReferenceImageToolbar ? "image-local-edit" : undefined}
referenceSize={25}
/>
)}
{aiWashQuickModeLabel}
@ -10329,6 +10355,7 @@ export function ImageNode({
{isSelectedImageActionInline("textEdit") ? (
<Button
data-tool-key="textEdit"
className="h-10 rounded-xl px-3 font-medium text-[13px] text-slate-700 hover:bg-slate-100 hover:text-slate-900 dark:text-slate-100 dark:hover:bg-zinc-800"
disabled={isProcessing || isDetectingText || isApplyingTextEdit}
onClick={() => handleAction("text-edit")}
@ -10339,6 +10366,8 @@ export function ImageNode({
className="mr-1.5 h-4 w-4"
fallback={Type}
item={imageTextEditItem}
referenceName={useReferenceImageToolbar ? "image-edit-text" : undefined}
referenceSize={25}
/>
{imageTextEditItem.label}
</Button>
@ -10346,6 +10375,7 @@ export function ImageNode({
{isSelectedImageActionInline("boxCutout") ? (
<Button
data-tool-key="boxCutout"
className="group relative h-10 rounded-xl px-3 font-medium text-[13px] text-slate-700 hover:bg-slate-100 hover:text-slate-900 dark:text-slate-100 dark:hover:bg-zinc-800"
disabled={isProcessing}
onClick={() => handleAction("box-cutout")}
@ -10356,6 +10386,8 @@ export function ImageNode({
className="mr-1.5 h-4 w-4"
fallback={Scan}
item={imageBoxCutoutItem}
referenceName={useReferenceImageToolbar ? "image-vector" : undefined}
referenceSize={25}
/>
{imageBoxCutoutItem.label}
<ImageActionCreditBadge compact label={boxCutoutCostShortLabel} />
@ -10364,6 +10396,7 @@ export function ImageNode({
{isSelectedImageActionInline("localEdit") ? (
<Button
data-tool-key="localEdit"
className="group relative h-10 rounded-xl px-3 font-medium text-[13px] text-slate-700 hover:bg-slate-100 hover:text-slate-900 dark:text-slate-100 dark:hover:bg-zinc-800"
disabled={isProcessing || isSubmittingLocalEdit}
onClick={() => handleAction("local-edit")}
@ -10374,6 +10407,8 @@ export function ImageNode({
className="mr-1.5 h-4 w-4"
fallback={Wand2}
item={imageLocalEditItem}
referenceName={useReferenceImageToolbar ? "image-local-edit" : undefined}
referenceSize={25}
/>
{imageLocalEditItem.label}
<ImageActionCreditBadge
@ -10385,6 +10420,7 @@ export function ImageNode({
{isSelectedImageActionInline("layoutReplace") ? (
<Button
data-tool-key="layoutReplace"
className="group relative h-10 rounded-xl px-3 font-medium text-[13px] text-slate-700 hover:bg-slate-100 hover:text-slate-900 dark:text-slate-100 dark:hover:bg-zinc-800"
disabled={isProcessing}
onClick={() => handleAction("layout-replace")}
@ -10406,6 +10442,7 @@ export function ImageNode({
{isSelectedImageActionInline("removeWatermark") ? (
<Button
data-tool-key="removeWatermark"
className="group relative h-10 rounded-xl px-3 font-medium text-[13px] text-slate-700 hover:bg-slate-100 hover:text-slate-900 dark:text-slate-100 dark:hover:bg-zinc-800"
disabled={isProcessing}
onClick={() => handleAction("watermark")}
@ -10416,6 +10453,8 @@ export function ImageNode({
className="mr-1.5 h-4 w-4"
fallback={Eraser}
item={imageRemoveWatermarkItem}
referenceName={useReferenceImageToolbar ? "image-erase" : undefined}
referenceSize={25}
/>
{imageRemoveWatermarkItem.label}
<ImageActionCreditBadge
@ -10427,6 +10466,7 @@ export function ImageNode({
{isSelectedImageActionInline("resize") ? (
<Button
data-tool-key="resize"
className="h-10 rounded-xl px-3 font-medium text-[13px] text-slate-700 hover:bg-slate-100 hover:text-slate-900 dark:text-slate-100 dark:hover:bg-zinc-800"
disabled={isProcessing}
onClick={() => handleAction("resize")}
@ -10444,6 +10484,7 @@ export function ImageNode({
{isSelectedImageActionInline("multiAngle") ? (
<Button
data-tool-key="multiAngle"
className="group relative h-10 rounded-xl px-3 font-medium text-[13px] text-slate-700 hover:bg-slate-100 hover:text-slate-900 dark:text-slate-100 dark:hover:bg-zinc-800"
disabled={isProcessing}
onClick={() => handleAction("angle")}
@ -10454,6 +10495,8 @@ export function ImageNode({
className="mr-1.5 h-4 w-4"
fallback={Box}
item={imageMultiAngleItem}
referenceName={useReferenceImageToolbar ? "image-angle" : undefined}
referenceSize={25}
/>
{imageMultiAngleItem.label}
<ImageActionCreditBadge
@ -10465,6 +10508,7 @@ export function ImageNode({
{isSelectedImageActionInline("generate3dModel") ? (
<Button
data-tool-key="generate3dModel"
className="group relative h-10 rounded-xl px-3 font-medium text-[13px] text-slate-700 hover:bg-slate-100 hover:text-slate-900 dark:text-slate-100 dark:hover:bg-zinc-800"
disabled={isProcessing}
onClick={() => handleAction("generate-3d-model")}
@ -10486,6 +10530,7 @@ export function ImageNode({
{isSelectedImageActionInline("generate3dWorld") ? (
<Button
data-tool-key="generate3dWorld"
className="group relative h-10 rounded-xl px-3 font-medium text-[13px] text-slate-700 hover:bg-slate-100 hover:text-slate-900 dark:text-slate-100 dark:hover:bg-zinc-800"
disabled={isProcessing}
onClick={() => handleAction("generate-3d-world")}
@ -10507,6 +10552,7 @@ export function ImageNode({
{isSelectedImageActionInline("layerSplit") ? (
<Button
data-tool-key="layerSplit"
className="h-10 rounded-xl px-3 font-medium text-[13px] text-slate-700 hover:bg-slate-100 hover:text-slate-900 dark:text-slate-100 dark:hover:bg-zinc-800"
disabled={isProcessing}
onClick={() => handleAction("layer-split")}
@ -10517,6 +10563,8 @@ export function ImageNode({
className="mr-1.5 h-4 w-4"
fallback={Layers}
item={imageLayerSplitItem}
referenceName={useReferenceImageToolbar ? "image-extract-layer" : undefined}
referenceSize={25}
/>
{imageLayerSplitItem.label}
</Button>
@ -10524,6 +10572,7 @@ export function ImageNode({
{isSelectedImageActionInline("extend") ? (
<Button
data-tool-key="extend"
className="group relative h-10 rounded-xl px-3 font-medium text-[13px] text-slate-700 hover:bg-slate-100 hover:text-slate-900 dark:text-slate-100 dark:hover:bg-zinc-800"
disabled={isProcessing}
onClick={() => handleAction("extend")}
@ -10534,6 +10583,8 @@ export function ImageNode({
className="mr-1.5 h-4 w-4"
fallback={Expand}
item={imageExtendItem}
referenceName={useReferenceImageToolbar ? "image-outpaint" : undefined}
referenceSize={25}
/>
{imageExtendItem.label}
<ImageActionCreditBadge compact label={extendCostShortLabel} />
@ -10542,6 +10593,7 @@ export function ImageNode({
{isSelectedImageActionInline("crop") ? (
<Button
data-tool-key="crop"
className="h-10 rounded-xl px-3 font-medium text-[13px] text-slate-700 hover:bg-slate-100 hover:text-slate-900 dark:text-slate-100 dark:hover:bg-zinc-800"
disabled={isProcessing}
onClick={() => handleAction("crop")}
@ -10552,6 +10604,8 @@ export function ImageNode({
className="mr-1.5 h-4 w-4"
fallback={Crop}
item={imageCropItem}
referenceName={useReferenceImageToolbar ? "image-crop" : undefined}
referenceSize={25}
/>
{imageCropItem.label}
</Button>
@ -10559,6 +10613,7 @@ export function ImageNode({
{isSelectedImageActionInline("adjust") ? (
<Button
data-tool-key="adjust"
className="h-10 rounded-xl px-3 font-medium text-[13px] text-slate-700 hover:bg-slate-100 hover:text-slate-900 dark:text-slate-100 dark:hover:bg-zinc-800"
disabled={isProcessing}
onClick={() => handleAction("adjust")}
@ -10569,6 +10624,8 @@ export function ImageNode({
className="mr-1.5 h-4 w-4"
fallback={SlidersHorizontal}
item={imageAdjustItem}
referenceName={useReferenceImageToolbar ? "image-adjust" : undefined}
referenceSize={25}
/>
{imageAdjustItem.label}
</Button>
@ -10576,6 +10633,7 @@ export function ImageNode({
{isSelectedImageActionInline("flipRotate") ? (
<Button
data-tool-key="flipRotate"
className="h-10 rounded-xl px-3 font-medium text-[13px] text-slate-700 hover:bg-slate-100 hover:text-slate-900 dark:text-slate-100 dark:hover:bg-zinc-800"
disabled={isProcessing}
onClick={() => handleAction("flip-rotate")}
@ -10586,6 +10644,8 @@ export function ImageNode({
className="mr-1.5 h-4 w-4"
fallback={FlipHorizontal2}
item={imageFlipRotateItem}
referenceName={useReferenceImageToolbar ? "image-flip-rotate" : undefined}
referenceSize={25}
/>
{imageFlipRotateItem.label}
</Button>
@ -10593,6 +10653,7 @@ export function ImageNode({
{isSelectedImageActionInline("promptRegenerate") ? (
<Button
data-tool-key="promptRegenerate"
className="group relative h-10 rounded-xl px-3 font-medium text-[13px] text-slate-700 hover:bg-slate-100 hover:text-slate-900 dark:text-slate-100 dark:hover:bg-zinc-800"
disabled={isProcessing}
onClick={() => handleAction("prompt-regenerate")}
@ -10614,6 +10675,7 @@ export function ImageNode({
{isSelectedImageActionInline("note") ? (
<Button
data-tool-key="note"
className="h-10 rounded-xl px-3 font-medium text-[13px] text-slate-700 hover:bg-slate-100 hover:text-slate-900 dark:text-slate-100 dark:hover:bg-zinc-800"
disabled={isProcessing}
onClick={() => handleAction("note")}
@ -10624,6 +10686,8 @@ export function ImageNode({
className="mr-1.5 h-4 w-4"
fallback={StickyNote}
item={imageNoteItem}
referenceName={useReferenceImageToolbar ? "image-mark" : undefined}
referenceSize={25}
/>
{imageNoteItem.label}
</Button>
@ -10648,12 +10712,14 @@ export function ImageNode({
className="h-4 w-4"
fallback={MoreHorizontal}
item={imageNodeMoreItem}
referenceName={useReferenceImageToolbar ? "image-more" : undefined}
referenceSize={25}
/>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent
align="center"
className="bg-white opacity-100 backdrop-blur-none dark:bg-zinc-950 dark:text-zinc-100"
className={cn("bg-white opacity-100 backdrop-blur-none dark:bg-zinc-950 dark:text-zinc-100", useReferenceImageToolbar && "lovart-image-action-menu")}
side="bottom"
>
{isSelectedImageActionInMore("boxCutout") ? (
@ -10662,6 +10728,8 @@ export function ImageNode({
className="mr-2 h-4 w-4"
fallback={Scan}
item={imageBoxCutoutItem}
referenceName={useReferenceImageToolbar ? "image-vector" : undefined}
referenceSize={18}
/>
<span>{imageBoxCutoutItem.label}</span>
<ImageActionCreditBadge label={boxCutoutCostShortLabel} />
@ -10673,6 +10741,8 @@ export function ImageNode({
className="mr-2 h-4 w-4"
fallback={Wand2}
item={imageLocalEditItem}
referenceName={useReferenceImageToolbar ? "image-local-edit" : undefined}
referenceSize={18}
/>
<span>{imageLocalEditItem.label}</span>
<ImageActionCreditBadge label={localEditCostShortLabel} />
@ -10697,6 +10767,8 @@ export function ImageNode({
className="mr-2 h-4 w-4"
fallback={Eraser}
item={imageRemoveWatermarkItem}
referenceName={useReferenceImageToolbar ? "image-erase" : undefined}
referenceSize={18}
/>
<span>{imageRemoveWatermarkItem.label}</span>
<ImageActionCreditBadge
@ -10720,6 +10792,8 @@ export function ImageNode({
className="mr-2 h-4 w-4"
fallback={Box}
item={imageMultiAngleItem}
referenceName={useReferenceImageToolbar ? "image-angle" : undefined}
referenceSize={18}
/>
<span>{imageMultiAngleItem.label}</span>
<ImageActionCreditBadge
@ -10761,6 +10835,8 @@ export function ImageNode({
className="mr-2 h-4 w-4"
fallback={Layers}
item={imageLayerSplitItem}
referenceName={useReferenceImageToolbar ? "image-extract-layer" : undefined}
referenceSize={18}
/>
{imageLayerSplitItem.label}
</DropdownMenuItem>
@ -10771,6 +10847,8 @@ export function ImageNode({
className="mr-2 h-4 w-4"
fallback={Expand}
item={imageExtendItem}
referenceName={useReferenceImageToolbar ? "image-outpaint" : undefined}
referenceSize={18}
/>
<span>{imageExtendItem.label}</span>
<ImageActionCreditBadge label={extendCostShortLabel} />
@ -10782,6 +10860,8 @@ export function ImageNode({
className="mr-2 h-4 w-4"
fallback={Crop}
item={imageCropItem}
referenceName={useReferenceImageToolbar ? "image-crop" : undefined}
referenceSize={18}
/>
{imageCropItem.label}
</DropdownMenuItem>
@ -10792,6 +10872,8 @@ export function ImageNode({
className="mr-2 h-4 w-4"
fallback={SlidersHorizontal}
item={imageAdjustItem}
referenceName={useReferenceImageToolbar ? "image-adjust" : undefined}
referenceSize={18}
/>
{imageAdjustItem.label}
</DropdownMenuItem>
@ -10802,6 +10884,8 @@ export function ImageNode({
className="mr-2 h-4 w-4"
fallback={FlipHorizontal2}
item={imageFlipRotateItem}
referenceName={useReferenceImageToolbar ? "image-flip-rotate" : undefined}
referenceSize={18}
/>
{imageFlipRotateItem.label}
</DropdownMenuItem>
@ -10827,6 +10911,8 @@ export function ImageNode({
className="mr-2 h-4 w-4"
fallback={StickyNote}
item={imageNoteItem}
referenceName={useReferenceImageToolbar ? "image-mark" : undefined}
referenceSize={18}
/>
{imageNoteItem.label}
</DropdownMenuItem>
@ -10866,7 +10952,8 @@ export function ImageNode({
void handleDownload();
}}
size="icon"
title={imageDownloadItem.label}
data-tool-key="download"
title={imageDownloadItem.label}
variant="ghost"
>
{isDownloadingOriginalImage ? (
@ -10876,11 +10963,13 @@ export function ImageNode({
className="h-4 w-4"
fallback={Download}
item={imageDownloadItem}
referenceName={useReferenceImageToolbar ? "image-download" : undefined}
referenceSize={25}
/>
)}
</Button>
) : null}
</div>
</ReferenceImageToolbar>
</TooltipProvider>,
document.body
)}

View File

@ -1,5 +1,7 @@
"use client";
import { ReferenceIcon, type ReferenceIconName } from "@/components/lovart/reference-icon";
import dynamic from "next/dynamic";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useRouter } from "next/navigation";
@ -1639,6 +1641,18 @@ export function LovartHome({
const FallbackIcon = LOVART_HOME_SIDEBAR_FALLBACK_ICONS[key];
const sizeClassName = options?.sizeClassName || "h-[18px] w-[18px]";
const referenceIcons: Partial<Record<LovartHomeSidebarItemKey, ReferenceIconName>> = {
newProject: "plus",
home: "home",
projects: "folder",
gallery: "brand",
agent: "skill",
};
const reference = referenceIcons[key];
if (reference && !item.iconUrl) {
return <ReferenceIcon name={reference} size={key === "newProject" ? 18 : 20} className={options?.filled ? "text-white" : undefined} />;
}
if (options?.filled) {
return (
<SidebarSvgIcon
@ -1704,7 +1718,7 @@ export function LovartHome({
const SelectedThemeIcon = selectedThemeOption.icon;
return (
<div className="min-h-svh overflow-x-hidden bg-white text-[#171717] dark:bg-[#0a0a0c] dark:text-zinc-100 [&_button]:cursor-pointer [&_input]:cursor-pointer [&_textarea]:cursor-pointer [&_img]:cursor-pointer [&_a]:cursor-pointer [&_[role=button]]:cursor-pointer">
<div className="lovart-home-surface min-h-svh overflow-x-hidden bg-white text-[#171717] dark:bg-[#0a0a0c] dark:text-zinc-100 [&_button]:cursor-pointer [&_input]:cursor-pointer [&_textarea]:cursor-pointer [&_img]:cursor-pointer [&_a]:cursor-pointer [&_[role=button]]:cursor-pointer">
{shouldShowHomeBanner ? (
<LovartPromoBanner
actionText={homeBannerActionText}
@ -1715,9 +1729,10 @@ export function LovartHome({
/>
) : null}
<BrandTopBar
className="!h-[40px]"
contentClassName="pl-[80px]"
logo={displayLogo}
offsetTop={shouldShowHomeBanner ? LOVART_PROMO_BANNER_HEIGHT : 0}
offsetTop={0}
onLogoClick={() => window.scrollTo({ top: 0, behavior: "smooth" })}
showBrand={false}
siteName={siteName || "Lovart"}
@ -1970,7 +1985,7 @@ export function LovartHome({
}
siteLogo={displayLogo}
siteName={siteName || "Lovart"}
topOffset={shouldShowHomeBanner ? LOVART_PROMO_BANNER_HEIGHT : 0}
topOffset={40 + (shouldShowHomeBanner ? LOVART_PROMO_BANNER_HEIGHT : 0)}
toolbox1Enabled={siteSettingsData?.toolbox1Enabled !== false}
toolbox1Links={toolbox1Links}
toolbox2Enabled={siteSettingsData?.toolbox2Enabled === true}
@ -1983,8 +1998,8 @@ export function LovartHome({
<main
className={cn(
"ml-[92px] mr-10 w-auto max-w-none pb-16 max-lg:mx-5",
shouldShowHomeBanner ? "pt-[113px]" : "pt-[68px]"
"ml-[120px] mr-[60px] w-auto max-w-none pb-16 max-lg:mx-5",
shouldShowHomeBanner ? "pt-[164px]" : "pt-[116px]"
)}
>
{!isProjectsOnlyView && (
@ -2013,25 +2028,15 @@ export function LovartHome({
setPrompt={setPrompt}
siteName={siteName}
selectedModelId={selectedComposerModel.id}
onOpenGallery={() => featuredSectionRef.current?.scrollIntoView({ behavior: "smooth", block: "start" })}
/>
)}
<LovartProjectsSection
deletingProjectId={deletingProjectId}
hasMoreProjects={hasMoreProjects}
isProjectsOnlyView={isProjectsOnlyView}
onCreateProject={openNewCanvas}
onDeleteProject={handleDeleteProject}
onOpenProject={openCanvasProject}
onPrepareCreate={preloadCanvasWorkspace}
onPrepareProject={prepareCanvasProject}
onViewAllProjects={openAllProjectsInPage}
projects={recentProjects}
projectSectionRef={projectSectionRef}
/>
{!isProjectsOnlyView && (
<LovartFeaturedGallery
onOpenProjects={openAllProjectsInPage}
onOpenMaterials={handleOpenImageEditor}
activeCategory={activeCategory}
categories={discoveryTabs}
featuredItems={featuredItems}
@ -2042,6 +2047,22 @@ export function LovartHome({
setActiveCategory={setActiveCategory}
/>
)}
{(isProjectsOnlyView || recentProjects.length > 0) && (
<LovartProjectsSection
deletingProjectId={deletingProjectId}
hasMoreProjects={hasMoreProjects}
isProjectsOnlyView={isProjectsOnlyView}
onCreateProject={openNewCanvas}
onDeleteProject={handleDeleteProject}
onOpenProject={openCanvasProject}
onPrepareCreate={preloadCanvasWorkspace}
onPrepareProject={prepareCanvasProject}
onViewAllProjects={openAllProjectsInPage}
projects={recentProjects}
projectSectionRef={projectSectionRef}
/>
)}
</main>
<button

View File

@ -1,8 +1,11 @@
import { ReferenceIcon } from "@/components/lovart/reference-icon";
import { Gem, Heart, Loader2, PlayCircle } from "lucide-react";
import type { Ref } from "react";
import type { FeaturedImage } from "./types";
type LovartFeaturedGalleryProps = {
onOpenProjects: () => void;
onOpenMaterials: () => void;
activeCategory: string;
categories: string[];
featuredItems: FeaturedImage[];
@ -16,7 +19,11 @@ type LovartFeaturedGalleryProps = {
function getFeaturedPreviewUrl(item: FeaturedImage) {
const source = item.thumbnailUrl || item.originalUrl;
if (!source || !/^https:\/\/yybb\.top\//i.test(source) || source.includes("?")) {
if (
!source ||
!/^https:\/\/yybb\.top\//i.test(source) ||
source.includes("?")
) {
return source;
}
@ -24,6 +31,8 @@ function getFeaturedPreviewUrl(item: FeaturedImage) {
}
export function LovartFeaturedGallery({
onOpenProjects,
onOpenMaterials,
activeCategory,
categories,
featuredItems,
@ -34,21 +43,44 @@ export function LovartFeaturedGallery({
setActiveCategory,
}: LovartFeaturedGalleryProps) {
return (
<section ref={featuredSectionRef} className="mt-14">
<h2 className="text-[16px] font-semibold leading-none text-[#1f1f1f] dark:text-white">
灵感发现
</h2>
<section
ref={featuredSectionRef}
className="lovart-discovery mt-16 scroll-mt-28"
aria-label="灵感发现"
>
<div className="flex items-center gap-2 overflow-x-auto pb-2 text-[13px] text-[#858585] dark:text-white/50">
<span className="inline-flex h-7 shrink-0 items-center gap-2 rounded-[8px] bg-[#ededed] px-3 text-[#252525] dark:bg-white/10 dark:text-white">
<ReferenceIcon name="skill" size={16} />
灵感发现
</span>
<button
type="button"
onClick={onOpenProjects}
className="inline-flex h-7 shrink-0 items-center gap-2 rounded-[8px] px-3 hover:bg-black/5 dark:hover:bg-white/10"
>
<ReferenceIcon name="folder" size={16} />
我的项目
</button>
<button
type="button"
onClick={onOpenMaterials}
className="inline-flex h-7 shrink-0 items-center gap-2 rounded-[8px] px-3 hover:bg-black/5 dark:hover:bg-white/10"
>
<ReferenceIcon name="brand" size={16} />
我的素材
</button>
</div>
<div className="mt-4 flex flex-wrap items-center gap-x-3 gap-y-2">
<div className="flex items-center gap-x-5 overflow-x-auto border-b border-[#ededed] px-3 dark:border-white/10">
{categories.map((category) => (
<button
key={category}
type="button"
onClick={() => setActiveCategory(category)}
className={`h-7 rounded-[7px] px-3 text-[12px] font-normal leading-none transition-colors duration-150 ${
className={`relative h-9 shrink-0 border-b px-0 text-[13px] font-normal leading-none transition-colors duration-150 ${
activeCategory === category
? "bg-[#f2f2f2] text-[#202020] dark:bg-[#24242a] dark:text-white"
: "text-[#5f5f5f] hover:bg-[#f7f7f7] hover:text-[#202020] dark:text-white/55 dark:hover:bg-[#17171b]"
? "border-[#303030] text-[#202020] dark:border-white dark:text-white"
: "border-transparent text-[#858585] hover:text-[#202020] dark:text-white/55 dark:hover:text-white"
}`}
>
{category}
@ -58,15 +90,19 @@ export function LovartFeaturedGallery({
{featuredItems.length === 0 ? (
<div className="mt-[22px] rounded-[10px] border border-dashed border-black/15 bg-[#f7f7f7] p-10 text-center text-[14px] text-black/35 dark:border-white/12 dark:bg-[#141416] dark:text-white/35">
{isLoadingFeatured ? "灵感内容加载中..." : "当前没有可展示的灵感数据。"}
{isLoadingFeatured
? "灵感内容加载中..."
: "当前没有可展示的灵感数据。"}
</div>
) : (
<div className="mt-[14px] columns-1 gap-[10px] md:columns-2 lg:columns-5">
<div className="mt-6 columns-1 gap-4 sm:columns-2 lg:columns-4">
{featuredItems.map((item, index) => (
<button
key={item.id}
type="button"
onClick={() => window.open(item.originalUrl || item.thumbnailUrl, "_blank")}
onClick={() =>
window.open(item.originalUrl || item.thumbnailUrl, "_blank")
}
className="group mb-[24px] block w-full cursor-pointer break-inside-avoid text-left"
>
<img
@ -78,14 +114,17 @@ export function LovartFeaturedGallery({
const image = event.currentTarget;
const fallbackUrl = item.originalUrl || item.thumbnailUrl;
if (!fallbackUrl || image.dataset.fallbackApplied === "true") {
if (
!fallbackUrl ||
image.dataset.fallbackApplied === "true"
) {
return;
}
image.dataset.fallbackApplied = "true";
image.src = fallbackUrl;
}}
className="block h-auto w-full rounded-[6px] bg-[#e8e8e8] object-cover transition-transform duration-150 ease-out will-change-transform group-hover:scale-[1.008] dark:bg-[#1b1c20]"
className="block h-auto w-full rounded-[12px] bg-[#e8e8e8] object-cover transition-transform duration-150 ease-out will-change-transform group-hover:scale-[1.008] dark:bg-[#1b1c20]"
/>
<div className="mt-[8px] flex h-[22px] items-center justify-between text-[11px] font-normal leading-none text-[#b8b8b8] dark:text-white/45">
<div className="flex min-w-0 items-center gap-[6px] pr-2">
@ -112,7 +151,10 @@ export function LovartFeaturedGallery({
))}
</div>
)}
<div ref={featuredLoadMoreRef} className="mt-3 flex h-10 items-center justify-center">
<div
ref={featuredLoadMoreRef}
className="mt-3 flex h-10 items-center justify-center"
>
{isLoadingMoreFeatured ? (
<div className="inline-flex items-center gap-2 text-sm text-black/45 dark:text-white/45">
<Loader2 className="h-3.5 w-3.5 animate-spin" />

View File

@ -1,6 +1,8 @@
import dynamic from "next/dynamic";
import { ArrowUp, Box, Lightbulb, Loader2, Plus, Sparkles, X } from "lucide-react";
import type { ChangeEvent } from "react";
import { ReferenceIcon } from "@/components/lovart/reference-icon";
import { ModelIdentityIcon } from "@/components/lovart/model-identity-icon";
import { Lightbulb, Loader2, X } from "lucide-react";
import { useRef, type ChangeEvent } from "react";
import { Button } from "@/components/ui/button";
import {
Select,
@ -52,23 +54,19 @@ type LovartHeroComposerProps = {
onModelChange: (modelId: string) => void;
onStartAgentMode: () => void;
selectedModelId: string;
onOpenGallery: () => void;
};
export function LovartHeroComposer({
composerPlaceholder,
displayLogo,
files,
handleAddFiles,
heroPromoActionText,
heroPromoText,
heroSubtitleText,
heroTitleText,
homeAnimationData,
isAgentModeActive,
isMembershipEnabled,
isSubmitting,
onSubmit,
openUpgradeDialog,
prompt,
removeFile,
setPrompt,
@ -77,63 +75,71 @@ export function LovartHeroComposer({
onModelChange,
onStartAgentMode,
selectedModelId,
onOpenGallery,
}: LovartHeroComposerProps) {
const textareaRef = useRef<HTMLTextAreaElement>(null);
const uploadRef = useRef<HTMLInputElement>(null);
const suggestions = [
{
label: "电商",
icon: "commerce" as const,
prompt: "为我的产品设计一张高转化的电商主图,突出产品卖点。",
},
{
label: "社交媒体",
icon: "social" as const,
prompt: "设计一张适合社交媒体发布的视觉海报。",
},
{
label: "品牌",
icon: "branding" as const,
prompt: "为我的品牌设计一套统一的视觉形象。",
},
{
label: "创意",
icon: "creativity" as const,
prompt: "创作一张富有想象力和视觉冲击力的创意作品。",
},
{
label: "网站",
icon: "website" as const,
prompt: "设计一个简洁、现代的网站首页视觉稿。",
},
];
return (
<section className="mt-6 text-center">
{isMembershipEnabled ? (
<button
type="button"
onClick={openUpgradeDialog}
className="inline-flex w-full max-w-[620px] items-center gap-1.5 rounded-[12px] bg-[#fde8bf] px-3 py-1.5 text-[11px] leading-5 text-black/80 transition-colors duration-150 hover:bg-[#f9dfac] sm:w-auto sm:rounded-full sm:text-[12px] dark:bg-[#3f3013] dark:text-[#f6dfaa] dark:hover:bg-[#493819]"
>
<span className="rounded-full bg-[#ff9d3d] px-2 py-0.5 text-[10px] font-semibold text-white">NEW</span>
<span className="line-clamp-2 min-w-0 flex-1 text-left sm:line-clamp-1">
{heroPromoText || "现在升级,立享 Kling 3.0 与 Nano Banana Pro,最高 5 折优惠"}
</span>
<span className="shrink-0 text-[#ff7a00]">{heroPromoActionText || "立即升级"}</span>
</button>
) : (
<div className="inline-flex w-full max-w-[620px] items-center gap-1.5 rounded-[12px] bg-[#fde8bf] px-3 py-1.5 text-[11px] leading-5 text-black/80 sm:w-auto sm:rounded-full sm:text-[12px] dark:bg-[#3f3013] dark:text-[#f6dfaa]">
<span className="rounded-full bg-[#ff9d3d] px-2 py-0.5 text-[10px] font-semibold text-white">NEW</span>
<span className="line-clamp-2 min-w-0 flex-1 text-left sm:line-clamp-1">
{heroPromoText || "现在升级,立享 Kling 3.0 与 Nano Banana Pro,最高 5 折优惠"}
</span>
</div>
)}
<section className="lovart-hero mt-6 text-center" aria-label="开始设计">
<h1 className="text-[20px] font-medium leading-7 tracking-[-0.02em] dark:text-white">
{heroTitleText || "你想设计什么?"}
</h1>
{heroSubtitleText ? (
<p className="mt-2 text-[13px] text-black/45 dark:text-white/45">
{heroSubtitleText}
</p>
) : null}
<div className="mt-7 flex items-center justify-center gap-1.5">
{displayLogo ? (
<img
src={displayLogo}
alt={siteName}
className="h-8 w-8 rounded-full object-cover"
/>
) : (
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-black text-white dark:bg-white dark:text-black">
<Sparkles className="h-3.5 w-3.5" />
</div>
)}
<h1 className="text-[28px] font-semibold leading-tight sm:text-[30px] dark:text-white">
{heroTitleText || `${siteName || "Lovart"} 让设计更简单`}
</h1>
</div>
<p className="mt-1.5 text-[12px] text-black/38 dark:text-white/48">
{heroSubtitleText || "懂你的设计代理,帮你搞定一切"}
</p>
<div className="mx-auto mt-7 w-full max-w-[760px] rounded-[16px] border border-[#e1e4e8] bg-white px-[18px] pb-[10px] pt-[14px] shadow-[0_8px_22px_rgba(0,0,0,0.055)] transition-shadow duration-150 focus-within:shadow-[0_10px_26px_rgba(0,0,0,0.075)] dark:border-white/10 dark:bg-[#141416] dark:shadow-[0_14px_38px_rgba(0,0,0,0.32)]">
<div className="lovart-home-composer mx-auto mt-7 w-full max-w-[760px] rounded-[24px] border border-[#e5e5e5] bg-white px-[16px] pb-[8px] pt-[14px] shadow-[0_2px_4px_rgba(0,0,0,0.04),0_8px_20px_rgba(0,0,0,0.025)] transition-shadow duration-150 focus-within:shadow-[0_3px_8px_rgba(0,0,0,0.07)] dark:border-white/12 dark:bg-[#141416]">
<Textarea
ref={textareaRef}
aria-label="描述你的设计需求"
value={prompt}
onChange={(event) => setPrompt(event.target.value)}
placeholder={composerPlaceholder}
className="min-h-[60px] resize-none border-0 bg-transparent px-0 !text-[16px] leading-6 text-[#20242a] shadow-none placeholder:text-[#aeb3bb] focus-visible:ring-0 md:!text-[14px] dark:text-white dark:placeholder:text-white/35"
className="min-h-[54px] resize-none border-0 bg-transparent px-0 !text-[16px] leading-6 text-[#20242a] shadow-none placeholder:text-[#b7b7b7] focus-visible:ring-0 md:!text-[14px] dark:text-white dark:placeholder:text-white/35"
/>
{files.length > 0 && (
<div className="mb-3 flex flex-wrap gap-2 px-1">
{files.map((item) => (
<div key={item.id} className="group relative h-11 w-11 overflow-hidden rounded-md border border-black/10 dark:border-white/10">
<img src={item.previewUrl} alt={item.file.name} className="h-full w-full object-cover" />
<div
key={item.id}
className="group relative h-11 w-11 overflow-hidden rounded-md border border-black/10 dark:border-white/10"
>
<img
src={item.previewUrl}
alt={item.file.name}
className="h-full w-full object-cover"
/>
<button
type="button"
className="absolute right-0 top-0 hidden h-5 w-5 items-center justify-center rounded-bl bg-black/60 text-white group-hover:flex"
@ -150,8 +156,10 @@ export function LovartHeroComposer({
<div className="flex min-h-[44px] items-center justify-between pt-2">
<div className="flex min-w-0 flex-1 items-center gap-4">
<label className="inline-flex h-8 w-8 cursor-pointer items-center justify-center rounded-full text-[#20242a] transition-colors duration-150 hover:bg-black/5 dark:text-white/72 dark:hover:bg-white/8">
<Plus className="h-4 w-4" strokeWidth={1.9} />
<ReferenceIcon name="plus" size={18} />
<input
ref={uploadRef}
aria-label="上传参考图"
type="file"
accept="image/*"
multiple
@ -186,7 +194,17 @@ export function LovartHeroComposer({
title="选择模型"
>
<div className="flex h-8 w-8 items-center justify-center">
<Box className="block h-4 w-4" strokeWidth={1.9} />
<ModelIdentityIcon
name={
modelOptions.find((model) => model.id === selectedModelId)
?.displayName || "auto"
}
avatarUrl={
modelOptions.find((model) => model.id === selectedModelId)
?.avatarUrl
}
size={18}
/>
</div>
</SelectTrigger>
<SelectContent
@ -201,18 +219,15 @@ export function LovartHeroComposer({
className="h-8 rounded-[7px] text-[12px] [&>span:first-child]:hidden [&>span:last-child]:block [&>span:last-child]:w-full"
>
<span className="flex min-w-0 items-center gap-2">
<span className="flex h-5 w-5 shrink-0 items-center justify-center overflow-hidden rounded-full border border-black/10 bg-white text-[10px] font-medium text-[#6b7280] dark:border-white/15 dark:bg-white/8 dark:text-white/70">
{model.avatarUrl ? (
<img
alt=""
className="h-full w-full object-cover"
src={model.avatarUrl}
/>
) : (
(model.displayName || "A").slice(0, 1)
)}
<ModelIdentityIcon
name={model.displayName}
identity={`${model.modelId || model.id} ${model.providerType || ""}`}
avatarUrl={model.avatarUrl}
size={18}
/>
<span className="min-w-0 flex-1 truncate">
{model.displayName}
</span>
<span className="min-w-0 flex-1 truncate">{model.displayName}</span>
</span>
</SelectItem>
))}
@ -220,19 +235,63 @@ export function LovartHeroComposer({
</Select>
<Button
onClick={onSubmit}
aria-label="开始生成"
disabled={isSubmitting || (!prompt.trim() && files.length === 0)}
className="h-9 w-9 rounded-full bg-[#eeeeef] p-0 text-[#c2c7cf] transition-colors duration-150 hover:bg-[#e5e6e8] hover:text-[#7f8794] dark:bg-white/10 dark:text-white/40 dark:hover:bg-white/16"
>
{isSubmitting ? (
<Loader2 className="h-4.5 w-4.5 animate-spin" />
) : (
<ArrowUp className="h-4 w-4" strokeWidth={1.8} />
<ReferenceIcon name="arrow-up" size={18} />
)}
</Button>
</div>
</div>
</div>
<div className="mx-auto mt-4 w-full max-w-[760px] space-y-3 px-4 text-left">
<div className="flex flex-wrap items-center gap-2">
<span className="mr-1 w-[96px] shrink-0 text-[13px] text-[#8b8b8b] dark:text-white/45">
{siteName} 帮你做:
</span>
{suggestions.map(({ label, icon, prompt: suggestion }) => (
<button
key={label}
type="button"
className="lovart-suggestion"
onClick={() => {
setPrompt(suggestion);
textareaRef.current?.focus();
}}
>
<ReferenceIcon name={icon} size={18} />
{label}
</button>
))}
</div>
<div className="flex flex-wrap items-center gap-2">
<span className="mr-1 w-[96px] shrink-0 text-[13px] text-[#8b8b8b] dark:text-white/45">
连接/收集灵感:
</span>
<button
type="button"
className="lovart-suggestion"
onClick={() => uploadRef.current?.click()}
>
<ReferenceIcon name="connections" size={18} />
上传我的参考素材
</button>
<button
type="button"
className="lovart-suggestion"
onClick={onOpenGallery}
>
<ReferenceIcon name="clipper" size={18} />
浏览灵感,发现更多创意
</button>
</div>
</div>
{homeAnimationData?.enabled && homeAnimationData?.htmlContent ? (
<div className="mt-6 w-full">
<HomeAnimationEmbed htmlContent={homeAnimationData.htmlContent} />

View File

@ -8,7 +8,7 @@ type LovartPromoBannerProps = {
text: string;
};
export const LOVART_PROMO_BANNER_HEIGHT = 45;
export const LOVART_PROMO_BANNER_HEIGHT = 48;
export function LovartPromoBanner({
actionText,
@ -20,7 +20,7 @@ export function LovartPromoBanner({
const showAction = Boolean(link?.trim() && actionText?.trim());
return (
<div className="fixed inset-x-0 top-0 z-[120] h-[45px] bg-[#ddff58] text-[#11130d]">
<div className="fixed inset-x-0 top-[40px] z-[120] h-[48px] bg-[#e4ff58] text-[#11130d]">
<div className="mx-auto flex h-full max-w-[1440px] items-center justify-center gap-3 px-11 sm:px-12">
<Sparkles className="hidden h-4 w-4 shrink-0 sm:block" />
<p className="min-w-0 truncate text-center text-[13px] font-normal sm:text-[15px]">

View File

@ -88,9 +88,9 @@ export function LovartSidebar({
"rounded-[12px] border border-black/8 bg-white/96 p-2 text-[#111827] shadow-[0_18px_44px_rgba(15,23,42,0.12)] backdrop-blur-xl dark:border-white/10 dark:bg-[#141416]/96 dark:text-white dark:shadow-[0_18px_44px_rgba(0,0,0,0.4)]";
const sidebarButtonClass = (active = false) =>
[
"group flex h-8 w-8 shrink-0 items-center justify-center rounded-[8px] text-[#202020] transition-colors duration-150 hover:bg-[#f5f5f5] dark:text-white dark:hover:bg-white/10",
"group flex h-[36px] w-[36px] shrink-0 items-center justify-center rounded-[8px] text-[#202020] transition-colors duration-150 hover:bg-[#f5f5f5] dark:text-white dark:hover:bg-white/10",
active
? "bg-[#f5f5f5] text-[#202020] dark:bg-white/10 dark:text-white"
? "bg-[#e5e5e5] text-[#202020] dark:bg-white/10 dark:text-white"
: "",
]
.filter(Boolean)
@ -164,15 +164,15 @@ export function LovartSidebar({
return (
<aside
className={[
"fixed bottom-0 left-0 top-0 z-[100] hidden w-[56px] flex-col items-center border-r lg:flex",
"fixed bottom-0 left-0 top-0 z-[100] hidden w-[60px] flex-col items-center border-r lg:flex",
isDark
? "border-white/10 bg-black"
: "border-[#e9e9e9] bg-white",
: "border-[#ededed] bg-[#f5f5f5]",
].join(" ")}
style={{ top: topOffset }}
>
<TooltipProvider delayDuration={120}>
<div className="flex h-full w-full flex-col items-center overflow-y-auto overflow-x-hidden px-2 pb-3 pt-3 [-ms-overflow-style:none] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden">
<div className="flex h-full w-full flex-col items-center overflow-y-auto overflow-x-hidden px-2 pb-3 pt-[16px] [-ms-overflow-style:none] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden">
<Tooltip>
<TooltipTrigger asChild>
<button
@ -197,14 +197,14 @@ export function LovartSidebar({
<TooltipContent side="right">{siteName}</TooltipContent>
</Tooltip>
<div className="mt-5 flex w-full flex-col items-center gap-3.5">
<div className="mt-[22px] flex w-full flex-col items-center gap-[2px]">
{isEnabled("newProject") ? (
<Tooltip>
<TooltipTrigger asChild>
<button
type="button"
onClick={onNewCanvas}
className={sidebarButtonClass()}
className={`${sidebarButtonClass()} mb-[14px] border border-[#e5e5e5] bg-white shadow-[0_1px_2px_rgba(0,0,0,0.04)] dark:bg-white/5`}
title={getItem("newProject").label}
>
{renderIcon("newProject", {

View File

@ -1,5 +1,6 @@
"use client";
import { ReferenceIcon } from "@/components/lovart/reference-icon";
import { useEffect, useState } from "react";
import { Button } from "@/components/ui/button";
import { ChevronDown, ChevronUp, Link2, Scan, SlidersHorizontal, Sparkles } from "lucide-react";
@ -37,6 +38,7 @@ interface ImageParamsBarProps {
hideImageCount?: boolean; // 隐藏图片数量按钮(仅个别页面需要)
hideAdvancedOptions?: boolean; // 隐藏高级设置按钮
inlineTheme?: "light" | "dark" | "minimal";
referencePresentation?: boolean;
showInlineDivider?: boolean;
// 🆕 图片生成模型特性配置(从模型配置中读取)
imageGenConfig?: {
@ -135,6 +137,7 @@ export function ImageParamsBar({
hideImageCount = false,
hideAdvancedOptions = false,
inlineTheme = "light",
referencePresentation = false,
showInlineDivider = true,
imageGenConfig,
}: ImageParamsBarProps) {
@ -493,12 +496,12 @@ export function ImageParamsBar({
<PopoverTrigger asChild>
<Button
variant="ghost"
className="h-8 shrink-0 rounded-full bg-[#f1f1f2] px-2.5 text-[13px] font-normal text-[#20242a] hover:bg-[#e9eaec] focus-visible:ring-0 focus-visible:ring-offset-0 dark:bg-[#242529] dark:text-zinc-100 dark:hover:bg-[#2c2d33]"
className={cn("h-8 shrink-0 rounded-full bg-[#f1f1f2] px-2.5 text-[13px] font-normal text-[#20242a] hover:bg-[#e9eaec] focus-visible:ring-0 focus-visible:ring-offset-0 dark:bg-[#242529] dark:text-zinc-100 dark:hover:bg-[#2c2d33]", referencePresentation && "lovart-image-params")}
title="生成参数"
aria-label={`比例:${params.aspectRatio},分辨率:${params.imageSize}`}
>
{qualityLabel} · {selectedAspectLabel} · {currentImageSize} · {selectedImageCount} img
{minimalParamsOpen ? (
<span className={referencePresentation ? "min-w-0 truncate text-[13px]" : undefined}>{qualityLabel} · {selectedAspectLabel} · {currentImageSize} · {selectedImageCount} {referencePresentation ? "张" : "img"}</span>
{referencePresentation ? <ReferenceIcon name="chevron-down" size={18} className={minimalParamsOpen ? "rotate-180" : undefined} /> : minimalParamsOpen ? (
<ChevronUp className="ml-1 h-3 w-3" />
) : (
<ChevronDown className="ml-1 h-3 w-3" />

View File

@ -0,0 +1,44 @@
"use client";
import { useState } from "react";
import { getReferenceModelIcon, ReferenceIcon } from "./reference-icon";
type ModelIdentityIconProps = {
name: string;
identity?: string;
avatarUrl?: string | null;
size?: number;
className?: string;
};
export function ModelIdentityIcon({
name,
identity,
avatarUrl,
size = 18,
className,
}: ModelIdentityIconProps) {
const [failedUrl, setFailedUrl] = useState<string | null>(null);
const reference = getReferenceModelIcon(`${name} ${identity || ""}`);
if (reference || !avatarUrl || failedUrl === avatarUrl) {
return (
<ReferenceIcon
name={reference || "model-auto"}
size={size}
className={className}
/>
);
}
return (
<img
alt=""
aria-hidden="true"
className={className}
src={avatarUrl}
width={size}
height={size}
style={{ width: size, height: size, objectFit: "contain", flexShrink: 0 }}
onError={() => setFailedUrl(avatarUrl)}
/>
);
}

View File

@ -0,0 +1,114 @@
import type { CSSProperties } from "react";
import { cn } from "@/lib/utils";
export type ReferenceIconName =
| "home"
| "folder"
| "skill"
| "brand"
| "plus"
| "model-auto"
| "arrow-up"
| "commerce"
| "social"
| "branding"
| "creativity"
| "website"
| "connections"
| "clipper"
| "cursor"
| "comment"
| "draw"
| "image"
| "layers"
| "history"
| "expand"
| "collapse"
| "reference"
| "skill-filled"
| "brand-filled"
| "pinterest"
| "close"
| "reference-image"
| "chevron-down"
| "points"
| "page"
| "model-banana"
| "model-gpt"
| "model-seedream"
| "model-luma"
| "model-flux"
| "model-midjourney"
| "model-ideogram"
| "model-recraft"
| "image-local-edit"
| "image-mark"
| "image-upscale"
| "image-remove-background"
| "image-erase"
| "image-extract-layer"
| "image-edit-text"
| "image-angle"
| "image-motion"
| "image-more"
| "image-download"
| "image-move-object"
| "image-mockup"
| "image-outpaint"
| "image-adjust"
| "image-crop"
| "image-vector"
| "image-flip-rotate"
| "toolbar-pin-outline"
| "toolbar-pin-filled"
| "toolbar-reset"
| "toolbar-close";
type ReferenceIconProps = {
name: ReferenceIconName;
size?: number;
className?: string;
style?: CSSProperties;
};
export function ReferenceIcon({
name,
size = 18,
className,
style,
}: ReferenceIconProps) {
const source = `url("/icons/lovart/${name}.svg")`;
return (
<span
aria-hidden="true"
data-reference-icon={name}
className={cn("inline-block shrink-0 align-middle", className)}
style={{
width: size,
height: size,
backgroundColor: "currentColor",
WebkitMaskImage: source,
maskImage: source,
WebkitMaskRepeat: "no-repeat",
maskRepeat: "no-repeat",
WebkitMaskPosition: "center",
maskPosition: "center",
WebkitMaskSize: "contain",
maskSize: "contain",
...style,
}}
/>
);
}
export function getReferenceModelIcon(name: string): ReferenceIconName | null {
if (/nano[\s-]*banana|gemini|imagen/i.test(name)) return "model-banana";
if (/gpt|openai|dall[\s-]*e/i.test(name)) return "model-gpt";
if (/seedream|doubao/i.test(name)) return "model-seedream";
if (/flux/i.test(name)) return "model-flux";
if (/luma|uni[\s-]*1/i.test(name)) return "model-luma";
if (/midjourney/i.test(name)) return "model-midjourney";
if (/ideogram|p[\s-]*image/i.test(name)) return "model-ideogram";
if (/recraft/i.test(name)) return "model-recraft";
return null;
}

View File

@ -0,0 +1,74 @@
"use client";
import { useSyncExternalStore } from "react";
export const IMAGE_TOOLBAR_STORAGE_KEY = "canvas.image-toolbar.v1";
const CHANGE_EVENT = "canvas-image-toolbar-preferences";
export type ImageToolbarPreferences = {
pinnedKeys: string[];
showNames: boolean;
};
export function parseImageToolbarPreferences(
raw: string | null
): ImageToolbarPreferences | null {
if (!raw) return null;
try {
const value = JSON.parse(raw);
if (
!value ||
!Array.isArray(value.pinnedKeys) ||
typeof value.showNames !== "boolean"
)
return null;
return {
pinnedKeys: [
...new Set<string>(
value.pinnedKeys.filter(
(key: unknown) => typeof key === "string" && key.length < 80
)
),
],
showNames: value.showNames,
};
} catch {
return null;
}
}
function snapshot() {
try {
return window.localStorage.getItem(IMAGE_TOOLBAR_STORAGE_KEY);
} catch {
return null;
}
}
function subscribe(listener: () => void) {
const onStorage = (event: StorageEvent) => {
if (!event.key || event.key === IMAGE_TOOLBAR_STORAGE_KEY) listener();
};
window.addEventListener("storage", onStorage);
window.addEventListener(CHANGE_EVENT, listener);
return () => {
window.removeEventListener("storage", onStorage);
window.removeEventListener(CHANGE_EVENT, listener);
};
}
export function useImageToolbarPreferences() {
const raw = useSyncExternalStore(subscribe, snapshot, () => null);
return parseImageToolbarPreferences(raw);
}
export function saveImageToolbarPreferences(
preferences: ImageToolbarPreferences
) {
try {
window.localStorage.setItem(
IMAGE_TOOLBAR_STORAGE_KEY,
JSON.stringify(preferences)
);
window.dispatchEvent(new Event(CHANGE_EVENT));
return true;
} catch {
return false;
}
}

View File

@ -0,0 +1,12 @@
# UI reference icons
These vector shapes were captured from the rendered Lovart UI at the user-selected reference:
https://www.lovart.ai/canvas?projectId=2rqrq08o10cg
Only SVG drawing elements and local fragment references are retained. No scripts, event attributes, external references, account data or authentication data are included.
The model families are GPT/OpenAI, Nano Banana, Seedream, Flux, Luma, Midjourney, Ideogram and Recraft. Model IDs, parameter handling and provider calls remain in the existing application code. Other model names retain their configured avatar, with a neutral icon when an avatar is unavailable.
Selected-image toolbar and overflow-menu shapes were captured from the same reference with a temporary test image: annotation, HD upscale, background removal, eraser, layer extraction, text editing, angle editing, motion, more, download, object move, mockup, outpaint, adjustment, crop, vector and flip/rotate. The toolbar keeps the existing configured actions and custom icon overrides.
The local editing icon is an application-neutral selection/brush drawing rather than the reference service’s branded quick-edit logo. Toolbar customization uses local icons and stores only tool order, pinned tools and label visibility in the browser.

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24"><path fill="currentColor" fill-opacity="0.9" d="M11.25 20V5.81l-4.72 4.72a.75.75 0 1 1-1.06-1.06l6-6 .056-.052a.75.75 0 0 1 1.004.052l6 6a.75.75 0 1 1-1.06 1.06l-4.72-4.72V20a.75.75 0 0 1-1.5 0" /></svg>

After

Width:  |  Height:  |  Size: 299 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24"><path fill="currentColor" fill-opacity="0.9" d="M13.621 13.23a4.11 4.11 0 1 1-3.007 1.31l1.067-.389a2.53 2.53 0 0 0 1.144-.844q.387-.075.796-.076M5.012 6.236c.345-.125.73-.057 1.011.18l5.454 4.574a1.028 1.028 0 0 1-.31 1.752l-6.688 2.435a1.027 1.027 0 0 1-1.364-1.144l1.237-7.01c.063-.36.315-.661.66-.787M13.364 2.837a1.03 1.03 0 0 1 .78-.103L20.1 4.33c.547.147.873.71.726 1.258l-1.595 5.955a1.03 1.03 0 0 1-1.26.726l-4.752-1.274a2.53 2.53 0 0 0-.778-1.154l-1.033-.866 1.479-5.514c.07-.263.242-.488.478-.624" /></svg>

After

Width:  |  Height:  |  Size: 613 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24"><path fill="currentColor" fill-opacity="0.9" d="M13.234 13.236a4.11 4.11 0 0 1 3.897 4.104l-.005.211a4.11 4.11 0 0 1-8.209 0l-.005-.21a4.11 4.11 0 0 1 4.11-4.11zm-.212 1.495a2.61 2.61 0 1 0 0 5.219 2.61 2.61 0 0 0 0-5.219m-8.51-8.495c.345-.126.73-.057 1.012.179l5.453 4.575c.28.236.414.604.35.966l-.032.133a1.03 1.03 0 0 1-.628.654L3.98 15.177l-.131.038c-.265.059-.542.01-.77-.135l-.111-.081a1.03 1.03 0 0 1-.352-.966l1.237-7.01c.064-.361.315-.662.66-.787m-.277 7.25 5.149-1.874-4.198-3.523zm9.53-10.649a1.03 1.03 0 0 1 .78-.102L20.5 4.33c.548.147.873.71.727 1.257l-1.596 5.955-.031.096a1.03 1.03 0 0 1-.447.528l-.091.046a1.03 1.03 0 0 1-.69.056l-5.954-1.595a1.03 1.03 0 0 1-.726-1.258l1.595-5.954c.07-.264.243-.488.479-.625m-.503 6.51 5.041 1.35 1.35-5.041-5.04-1.35z" /></svg>

After

Width:  |  Height:  |  Size: 874 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 18 18"><path fill="currentColor" d="M11.588 2.25c.62 0 1.12 0 1.523.032.41.034.77.105 1.103.275.53.27.96.7 1.23 1.229.169.333.24.693.274 1.103.033.403.032.903.032 1.523v5.176c0 .62 0 1.12-.032 1.523-.034.41-.105.77-.275 1.103-.27.53-.7.96-1.229 1.23-.333.169-.693.24-1.103.274-.403.033-.903.032-1.523.032H6.412c-.62 0-1.12 0-1.523-.032-.41-.034-.77-.105-1.103-.275-.53-.27-.96-.7-1.23-1.229-.169-.333-.24-.693-.274-1.103-.033-.403-.032-.903-.032-1.523V6.412c0-.62 0-1.12.032-1.523.034-.41.105-.77.275-1.103.27-.53.7-.96 1.229-1.23.333-.169.693-.24 1.103-.274.403-.033.903-.032 1.523-.032zM9.563 14.625h2.025c.639 0 1.085 0 1.432-.029.34-.027.536-.08.684-.155.317-.162.575-.42.737-.737.076-.149.128-.344.155-.684.029-.347.029-.793.029-1.432v-.338H9.563zm-6.188-6.75v3.713c0 .639 0 1.085.029 1.432.027.34.08.536.155.684.162.317.42.575.737.737.149.076.344.128.684.155.347.029.793.029 1.432.029h2.026v-6.75zm6.188 2.25h5.062V6.412c0-.639 0-1.085-.029-1.432-.027-.34-.08-.535-.155-.684a1.7 1.7 0 0 0-.737-.737c-.149-.076-.344-.128-.684-.155-.347-.029-.793-.029-1.432-.029H9.563zm-3.15-6.75c-.64 0-1.086 0-1.433.029-.34.027-.535.08-.684.155-.317.162-.575.42-.737.737-.076.149-.128.344-.155.684-.029.347-.029.793-.029 1.432v.338h5.063V3.375z" /></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24"><path fill="currentColor" fill-opacity="0.9" d="M15.47 9.47a.75.75 0 1 1 1.06 1.06l-2.94 2.94a2.25 2.25 0 0 1-3.18 0l-2.94-2.94a.75.75 0 1 1 1.06-1.06l2.94 2.94a.75.75 0 0 0 1.06 0z" /></svg>

After

Width:  |  Height:  |  Size: 287 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 18 18"><path fill="currentColor" d="M10.688 9.75a.562.562 0 1 1 0 1.125H7.312a.562.562 0 1 1 0-1.125z" /><path fill="currentColor" fill-rule="evenodd" d="M14.16 3.059a.563.563 0 0 1 .93.632L13.006 6.75h1.431c.311 0 .563.252.563.563v6.375a2.813 2.813 0 0 1-2.812 2.812H5.812A2.813 2.813 0 0 1 3 13.688V7.312c0-.31.252-.562.563-.562h8.093a.6.6 0 0 1 .053-.094zM4.126 13.687c0 .933.756 1.688 1.688 1.688h6.375c.931 0 1.687-.755 1.687-1.687V7.875h-9.75z" clip-rule="evenodd" /><path fill="currentColor" d="M5.356 2.786a.3.3 0 0 1 .537 0l.312.624a.3.3 0 0 0 .134.134l.624.312a.3.3 0 0 1 0 .537l-.358.179-.266.133a.3.3 0 0 0-.134.134l-.312.624-.023.039a.3.3 0 0 1-.49 0l-.024-.039-.312-.624a.3.3 0 0 0-.092-.109l-.042-.025-.624-.312a.3.3 0 0 1 0-.537l.624-.312a.3.3 0 0 0 .134-.134zM9.415 1.42a.375.375 0 0 1 .67 0l.36.718a.38.38 0 0 0 .167.168l.717.359a.375.375 0 0 1 0 .67l-.717.36a.38.38 0 0 0-.168.167l-.359.717a.375.375 0 0 1-.67 0l-.36-.717a.4.4 0 0 0-.115-.136l-.052-.032-.717-.359a.375.375 0 0 1 0-.67l.717-.36a.38.38 0 0 0 .168-.167z" /></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24"><path fill="currentColor" fill-opacity="0.9" d="M15.005 7.934a.75.75 0 0 1 1.061 1.06L13.06 12l3.006 3.005a.75.75 0 0 1-1.06 1.061L12 13.06l-3.005 3.006a.75.75 0 0 1-1.06-1.06L10.938 12 7.934 8.995a.75.75 0 1 1 1.06-1.06L12 10.938z" /></svg>

After

Width:  |  Height:  |  Size: 337 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24"><path fill="currentColor" fill-opacity="0.9" d="M18 3.25A3.75 3.75 0 0 1 21.75 7v10A3.75 3.75 0 0 1 18 20.75H6A3.75 3.75 0 0 1 2.25 17V7A3.75 3.75 0 0 1 6 3.25zm-8.75 16H18A2.25 2.25 0 0 0 20.25 17V7A2.25 2.25 0 0 0 18 4.75H9.25zM6 4.75A2.25 2.25 0 0 0 3.75 7v10A2.25 2.25 0 0 0 6 19.25h1.75V4.75z" /></svg>

After

Width:  |  Height:  |  Size: 403 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24"><path fill="currentColor" fill-opacity="0.9" d="M20.5 12c0-4.38-3.766-8-8.5-8s-8.5 3.62-8.5 8c0 1.372.367 2.662 1.014 3.791.482.842.853 1.945.524 3.07q-.076.26-.161.517a16 16 0 0 0 1.241-.205c.922-.19 1.86-.072 2.7.248A9 9 0 0 0 12 20c4.734 0 8.5-3.62 8.5-8m1.5 0c0 5.285-4.517 9.5-10 9.5-1.31 0-2.564-.24-3.714-.677-.615-.233-1.262-.305-1.865-.18a18.5 18.5 0 0 1-2.597.34.75.75 0 0 1-.747-1.016c.196-.51.373-1.018.522-1.527l.027-.106c.116-.537-.047-1.157-.414-1.797A9.1 9.1 0 0 1 2 12c0-5.285 4.517-9.5 10-9.5S22 6.715 22 12" /></svg>

After

Width:  |  Height:  |  Size: 631 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 18 18"><path fill="currentColor" d="M13.652 2.25c1.27 0 2.383.852 2.715 2.078l.416 1.538a2.52 2.52 0 0 1-.846 2.629v4.443a2.813 2.813 0 0 1-2.813 2.812H5.25a2.81 2.81 0 0 1-2.812-2.812V8.495a2.52 2.52 0 0 1-.846-2.629l.416-1.538A2.81 2.81 0 0 1 4.722 2.25zm-.991 5.928c-.419.5-1.035.822-1.738.822a2.26 2.26 0 0 1-1.736-.821c-.418.5-1.035.82-1.737.821-.702 0-1.32-.322-1.738-.822a2.4 2.4 0 0 1-2.15.799v3.96c0 .933.755 1.688 1.687 1.688h7.875c.932 0 1.688-.756 1.688-1.687V8.977a2.394 2.394 0 0 1-2.151-.798M4.72 3.375c-.761 0-1.43.51-1.629 1.247l-.416 1.54v.002c-.243.885.4 1.711 1.212 1.711.713 0 1.263-.522 1.263-1.246v-.01a.563.563 0 0 1 1.125.006v.008c.002.708.548 1.242 1.174 1.242.627 0 1.174-.536 1.174-1.246v-.008a.563.563 0 0 1 1.125 0v.008c0 .71.547 1.246 1.174 1.246s1.174-.536 1.174-1.246v-.008a.563.563 0 0 1 1.125 0v.008c0 .724.55 1.246 1.263 1.246.811 0 1.456-.826 1.213-1.71v-.003l-.417-1.54a1.69 1.69 0 0 0-1.63-1.247z" /></svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 18 18"><path fill="currentColor" d="M2.49 10.664a2.623 2.623 0 0 1 3.442 3.804 5.6 5.6 0 0 0 3.065.907q.542 0 1.055-.099a.562.562 0 0 1 .21 1.106q-.617.117-1.265.118a6.72 6.72 0 0 1-3.932-1.264 2.623 2.623 0 0 1-2.574-4.572m10.806-5.408a.563.563 0 0 1 .794.055 6.73 6.73 0 0 1 1.58 5.46 2.624 2.624 0 0 1-2.79 4.44 2.625 2.625 0 0 1 1.712-4.866q.032-.296.033-.599a5.6 5.6 0 0 0-1.383-3.696.563.563 0 0 1 .054-.794m-8.194 6.932a1.5 1.5 0 1 0-2.598 1.5 1.5 1.5 0 0 0 2.598-1.5m9.84-.55a1.5 1.5 0 1 0-1.5 2.6 1.5 1.5 0 0 0 1.5-2.6M9 1.314a2.625 2.625 0 1 1-2.51 3.391 5.65 5.65 0 0 0-2.843 3.282.563.563 0 1 1-1.068-.35 6.77 6.77 0 0 1 3.835-4.131A2.62 2.62 0 0 1 9 1.313m0 1.125a1.5 1.5 0 1 0 0 3 1.5 1.5 0 0 0 0-3" /></svg>

After

Width:  |  Height:  |  Size: 811 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 18 18"><path fill="currentColor" fill-rule="evenodd" d="M12.727 2.602a1.888 1.888 0 0 1 2.67 2.67L5.272 15.399a1.888 1.888 0 1 1-2.67-2.67zm1.875.796a.763.763 0 0 0-1.08 0l-2.04 2.04 1.08 1.08 2.04-2.04a.763.763 0 0 0 0-1.08m-2.835 3.915-1.08-1.08-7.29 7.29a.763.763 0 1 0 1.08 1.08z" clip-rule="evenodd" /><path fill="currentColor" d="M7.387 1.556a.38.38 0 0 0 .168-.168l.359-.717a.375.375 0 0 1 .67 0l.36.717a.38.38 0 0 0 .167.168l.717.359a.375.375 0 0 1 0 .67l-.717.36a.38.38 0 0 0-.168.167l-.359.717a.375.375 0 0 1-.67 0l-.36-.717a.38.38 0 0 0-.167-.168l-.717-.358a.375.375 0 0 1 0-.671zM14.137 9.806a.38.38 0 0 0 .168-.168l.359-.717a.375.375 0 0 1 .67 0l.36.717a.38.38 0 0 0 .167.168l.717.359a.375.375 0 0 1 0 .67l-.717.36a.38.38 0 0 0-.168.167l-.359.717a.375.375 0 0 1-.67 0l-.36-.717a.38.38 0 0 0-.167-.168l-.717-.359a.375.375 0 0 1 0-.67zM3.637 5.306a.38.38 0 0 0 .168-.168l.359-.717a.375.375 0 0 1 .67 0l.36.717a.38.38 0 0 0 .167.168l.717.359a.375.375 0 0 1 0 .67l-.717.36a.38.38 0 0 0-.168.167l-.359.717a.375.375 0 0 1-.67 0l-.36-.717a.38.38 0 0 0-.167-.168l-.717-.358a.375.375 0 0 1 0-.671z" /></svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24"><path fill="currentColor" fill-opacity="0.9" d="M2.8 5.66C2.187 3.886 3.887 2.186 5.66 2.8l14.833 5.126c1.96.677 2.038 3.42.12 4.208l-5.722 2.35a.75.75 0 0 0-.408.409l-2.35 5.721-.08.174c-.849 1.682-3.307 1.611-4.059-.116l-.07-.178zm2.37-1.444a.75.75 0 0 0-.953.954l5.127 14.833c.225.653 1.14.68 1.402.04l2.35-5.72.096-.204c.245-.46.645-.823 1.131-1.023l5.72-2.35c.64-.263.614-1.177-.04-1.403z" /></svg>

After

Width:  |  Height:  |  Size: 499 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24"><path fill="currentColor" fill-opacity="0.9" d="M14.342 3.047c.97-.168 1.806.116 2.472.825.564.6.658 1.343.502 2.057-.149.68-.53 1.384-.998 2.066-.938 1.368-2.405 2.906-3.769 4.354-1.398 1.485-2.68 2.87-3.38 4.011-.353.578-.492.991-.496 1.261a.47.47 0 0 0 .058.257c.037.062.11.148.27.24.436.251 1.05.18 2.011-.46.933-.62 1.932-1.607 2.963-2.641.994-.997 2.04-2.063 2.986-2.69.472-.313 1.007-.578 1.564-.617.617-.043 1.175.198 1.598.713l.102.136c.471.694.313 1.496.073 2.105-.26.662-.718 1.372-1.12 1.998-.426.665-.798 1.25-1.004 1.769-.21.53-.154.758-.072.877.091.13.177.17.277.185.129.02.33 0 .61-.107.566-.215 1.216-.684 1.756-1.171a.75.75 0 0 1 1.005 1.113c-.593.535-1.406 1.148-2.228 1.46-.413.157-.888.26-1.365.189a1.88 1.88 0 0 1-1.285-.81c-.527-.755-.357-1.62-.092-2.288.27-.68.732-1.398 1.135-2.027.428-.667.793-1.24.987-1.736.199-.506.12-.67.063-.74l-.001-.001c-.129-.156-.223-.176-.334-.168-.173.012-.447.11-.841.371-.788.522-1.709 1.451-2.751 2.497-1.005 1.008-2.113 2.112-3.194 2.832-.988.657-2.199 1.154-3.36.63l-.231-.12a2.2 2.2 0 0 1-.814-.776 1.97 1.97 0 0 1-.265-1.041c.01-.68.321-1.378.716-2.023.798-1.303 2.205-2.81 3.568-4.257 1.398-1.485 2.767-2.924 3.623-4.173.43-.626.683-1.14.77-1.539.08-.365.01-.559-.13-.708-.323-.343-.651-.456-1.124-.375-.547.095-1.295.458-2.327 1.206-2.054 1.49-4.892 4.246-8.996 8.302a.75.75 0 0 1-1.055-1.066c4.069-4.022 7.002-6.876 9.17-8.45 1.08-.782 2.056-1.315 2.953-1.47" /></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24"><path fill="currentColor" fill-opacity="0.9" d="M9.47 13.47a.75.75 0 1 1 1.06 1.06l-4.72 4.72H10a.75.75 0 0 1 0 1.5H4a.75.75 0 0 1-.75-.75v-6a.75.75 0 0 1 1.5 0v4.19zM19.25 10V5.81l-4.72 4.72a.75.75 0 1 1-1.06-1.06l4.72-4.72H14a.75.75 0 0 1 0-1.5h6a.75.75 0 0 1 .75.75v6a.75.75 0 0 1-1.5 0" /></svg>

After

Width:  |  Height:  |  Size: 395 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24"><path fill="currentColor" fill-opacity="0.9" d="M20 11.404H4v5.846a2.25 2.25 0 0 0 2.25 2.25h11.5A2.25 2.25 0 0 0 20 17.25zm1.5 5.846A3.75 3.75 0 0 1 17.75 21H6.25a3.75 3.75 0 0 1-3.75-3.75v-12A2.25 2.25 0 0 1 4.75 3h4.6c.287 0 .569.07.822.205l2.015 1.072h7.063a2.25 2.25 0 0 1 2.25 2.25zM4 9.904h16V6.527a.75.75 0 0 0-.75-.75h-7.437L9.468 4.53A.25.25 0 0 0 9.35 4.5H4.75a.75.75 0 0 0-.75.75z" /></svg>

After

Width:  |  Height:  |  Size: 498 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24"><path fill="currentColor" fill-opacity="0.9" d="M15.47 9.47a.75.75 0 1 1 1.06 1.06l-2.94 2.94a2.25 2.25 0 0 1-3.18 0l-2.94-2.94a.75.75 0 1 1 1.06-1.06l2.94 2.94a.75.75 0 0 0 1.06 0z" /></svg>

After

Width:  |  Height:  |  Size: 287 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24"><path fill="currentColor" fill-opacity="0.9" d="M11.25 13.947a.75.75 0 0 1 1.5 0v5.603h5.083a1.917 1.917 0 0 0 1.917-1.917V9.324c0-.645-.325-1.247-.864-1.601L12.32 3.408a.58.58 0 0 0-.64 0L5.114 7.723c-.539.354-.864.956-.864 1.601v8.309c0 1.058.858 1.917 1.917 1.917h5.083zm10 3.686a3.417 3.417 0 0 1-3.417 3.417H6.167a3.417 3.417 0 0 1-3.417-3.417V9.324c0-1.15.579-2.223 1.54-2.855l6.565-4.315a2.08 2.08 0 0 1 2.29 0L19.71 6.47a3.42 3.42 0 0 1 1.54 2.855z" /></svg>

After

Width:  |  Height:  |  Size: 562 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24"><path fill="currentColor" fill-opacity="0.9" d="M7 17.5c.698 0 1.298.41 1.58 1h12.67a.75.75 0 0 1 0 1.5H8.58a1.748 1.748 0 0 1-3.16 0H2.75a.75.75 0 0 1 0-1.5h2.67c.282-.59.882-1 1.58-1M19.25 4A2.75 2.75 0 0 1 22 6.75v7a2.75 2.75 0 0 1-2.75 2.75H4.75A2.75 2.75 0 0 1 2 13.75v-7A2.75 2.75 0 0 1 4.75 4zM8.12 10.498a1.25 1.25 0 0 0-1.456-.108L3.5 12.322v1.428c0 .69.56 1.25 1.25 1.25h8.736zM4.75 5.5c-.69 0-1.25.56-1.25 1.25v3.815L5.882 9.11a2.75 2.75 0 0 1 3.201.24l6.65 5.577a1 1 0 0 1 .075.074h3.442c.69 0 1.25-.56 1.25-1.25v-7c0-.69-.56-1.25-1.25-1.25zM16.25 7a2.25 2.25 0 1 1 0 4.5 2.25 2.25 0 0 1 0-4.5m0 1.5a.75.75 0 1 0 0 1.5.75.75 0 0 0 0-1.5" /></svg>

After

Width:  |  Height:  |  Size: 754 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24"><path fill="currentColor" fill-opacity="0.9" d="M11.22 1.546a3.75 3.75 0 0 1 1.56 0c.608.13 1.164.461 1.994.94l4.077 2.354c.83.48 1.396.796 1.812 1.258.352.39.618.85.78 1.35.192.592.184 1.24.184 2.199v4.707c0 .959.009 1.606-.184 2.198-.162.5-.429.96-.78 1.35-.416.463-.982.78-1.812 1.258l-4.077 2.354c-.83.48-1.386.811-1.995.94-.514.11-1.046.11-1.56 0-.456-.097-.883-.308-1.417-.609l-.578-.331-4.076-2.354c-.83-.479-1.396-.795-1.812-1.257a3.75 3.75 0 0 1-.78-1.35c-.192-.593-.183-1.24-.183-2.2V9.648c0-.959-.009-1.607.183-2.198.163-.5.428-.96.78-1.351.416-.462.982-.778 1.812-1.258l4.076-2.353c.83-.48 1.387-.811 1.995-.94m1.529 10.887v8.466c.264-.106.614-.302 1.275-.684l4.077-2.353c.918-.53 1.228-.72 1.448-.963.21-.235.37-.511.467-.81.102-.313.11-.675.11-1.735V9.647c0-.764-.006-1.166-.046-1.447zm-4.125 7.003 1.35.78c.66.38 1.011.576 1.275.682v-4.086zm-3.168-1.832.442.258 1.382.797 3.969-3.968v-2.258l-.395-.228zm-1.583-3.416v.166c0 1.06.009 1.422.11 1.734.067.207.166.401.288.58l5.24-5.24-1.826-1.054zM3.918 8.2c-.04.281-.045.683-.045 1.447v2.42L6.34 9.598zm8.55-5.187a2.25 2.25 0 0 0-.936 0c-.32.068-.64.243-1.558.772L5.898 6.14c-.66.381-1.007.586-1.23.762l2.77 1.6.032-.031.114.114 4.415 2.55 7.33-4.233c-.223-.176-.568-.381-1.228-.762l-4.077-2.354c-.917-.53-1.235-.704-1.556-.772" /></svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24"><path fill="currentColor" fill-opacity="0.9" d="M17.5 8.75a2.25 2.25 0 0 0-2.25-2.25H6.5v8.75a2.25 2.25 0 0 0 2.25 2.25h8.75zM19 17.5h2.25a.75.75 0 0 1 0 1.5H19v2.25a.75.75 0 0 1-1.5 0V19H8.75A3.75 3.75 0 0 1 5 15.25V6.5H2.75a.75.75 0 0 1 0-1.5H5V2.75a.75.75 0 0 1 1.5 0V5h8.75A3.75 3.75 0 0 1 19 8.75z" /></svg>

After

Width:  |  Height:  |  Size: 408 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24"><path fill="currentColor" fill-opacity="0.9" d="M3 17.25v-2.5a.75.75 0 0 1 1.5 0v2.5a2.25 2.25 0 0 0 2.25 2.25h10.5a2.25 2.25 0 0 0 2.25-2.25v-2.5a.75.75 0 0 1 1.5 0v2.5A3.75 3.75 0 0 1 17.25 21H6.75A3.75 3.75 0 0 1 3 17.25m8.25-13.5a.75.75 0 0 1 1.5 0v9.44l2.22-2.22a.75.75 0 1 1 1.06 1.06l-3.5 3.5a.75.75 0 0 1-1.06 0l-3.5-3.5a.75.75 0 1 1 1.06-1.06l2.22 2.22z" /></svg>

After

Width:  |  Height:  |  Size: 468 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24"><path fill="currentColor" fill-opacity="0.9" d="M15.45 3c.828 0 1.494-.001 2.031.043.547.045 1.027.14 1.471.366a3.75 3.75 0 0 1 1.639 1.639c.226.444.321.924.366 1.47.044.538.043 1.204.043 2.032v1.95a.75.75 0 0 1-1.5 0V8.55c0-.853 0-1.447-.038-1.91-.037-.453-.107-.714-.207-.911a2.25 2.25 0 0 0-.983-.984c-.198-.1-.459-.17-.913-.207-.462-.038-1.057-.038-1.909-.038h-6.9c-.853 0-1.447 0-1.91.038-.453.037-.714.106-.911.207a2.25 2.25 0 0 0-.984.984c-.1.197-.17.458-.207.912C4.5 7.103 4.5 7.697 4.5 8.55v6.9c0 .852 0 1.447.038 1.91.037.453.106.714.207.912.216.423.56.767.984.983.197.1.458.17.912.207.462.038 1.056.038 1.909.038h1.95a.75.75 0 0 1 0 1.5H8.55c-.828 0-1.494.001-2.031-.043-.547-.045-1.027-.14-1.471-.366a3.75 3.75 0 0 1-1.639-1.639c-.226-.444-.321-.924-.366-1.47C2.999 16.943 3 16.277 3 15.45v-6.9c0-.828-.001-1.494.043-2.031.045-.547.14-1.027.366-1.471a3.75 3.75 0 0 1 1.639-1.639c.444-.226.924-.321 1.47-.366C7.057 2.999 7.723 3 8.55 3zm1.52 10.637a2.4 2.4 0 1 1 3.393 3.393l-3.75 3.75a.75.75 0 0 1-.53.22H13.75a.75.75 0 0 1-.75-.75v-2.333c0-.199.08-.39.22-.53zm2.333 1.06a.9.9 0 0 0-1.273 0l-3.53 3.53V19.5h1.273l3.53-3.53a.9.9 0 0 0 0-1.273M16 7.25a.75.75 0 0 1 0 1.5h-3.25V15a.75.75 0 0 1-1.5 0V8.75H8a.75.75 0 0 1 0-1.5z" /></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24"><path fill="currentColor" fill-opacity="0.9" d="M11.606 3.348a3.75 3.75 0 0 1 5.303 0l3.757 3.757a3.75 3.75 0 0 1 0 5.304l-8.257 8.257c-1.427 1.463-3.845 1.458-5.303 0l-3.757-3.757a3.75 3.75 0 0 1 0-5.304zm-7.197 9.318a2.25 2.25 0 0 0 0 3.182l3.757 3.757c.882.882 2.343.865 3.174.009l1.132-1.132-6.94-6.94zm11.44-8.257a2.25 2.25 0 0 0-3.183 0l-6.073 6.073 6.94 6.94 6.073-6.074a2.25 2.25 0 0 0 0-3.182z" /></svg>

After

Width:  |  Height:  |  Size: 508 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24"><path fill="currentColor" fill-opacity="0.9" d="M11.223 3.183a1.75 1.75 0 0 1 1.554 0l8.25 4.104c1.297.646 1.297 2.505 0 3.15L17.886 12l3.14 1.563c1.297.645 1.297 2.504 0 3.15l-8.25 4.104a1.75 1.75 0 0 1-1.554 0l-8.25-4.104c-1.297-.646-1.297-2.505 0-3.15L6.114 12l-3.14-1.562c-1.297-.646-1.297-2.505 0-3.15zm1.554 11.359a1.74 1.74 0 0 1-1.554 0l-3.426-1.704-4.155 2.067a.26.26 0 0 0 0 .464l8.249 4.106c.069.034.15.034.218 0l8.25-4.106a.26.26 0 0 0 0-.464l-4.157-2.067zM12.11 4.525a.25.25 0 0 0-.218 0L3.64 8.631a.26.26 0 0 0 0 .464l8.25 4.104c.069.034.15.034.218 0l8.25-4.104a.26.26 0 0 0 0-.464z" /></svg>

After

Width:  |  Height:  |  Size: 702 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24"><path fill="currentColor" fill-opacity="0.9" d="M12 17.5a.75.75 0 0 1 .75.75v2a.75.75 0 0 1-1.5 0v-2a.75.75 0 0 1 .75-.75M7 7.768a.5.5 0 0 1 .82-.384l1.72 1.431a.5.5 0 0 1 0 .769l-1.72 1.433a.5.5 0 0 1-.82-.384v-.598c-1.073.255-1.943.582-2.559.942-.776.455-.941.832-.941 1.023 0 .137.077.374.456.688.374.311.961.625 1.752.903 1.575.553 3.8.909 6.292.909s4.717-.356 6.292-.91c.79-.277 1.378-.59 1.752-.902.379-.314.456-.55.456-.688 0-.219-.221-.662-1.203-1.166-.916-.47-2.265-.865-3.903-1.1a.75.75 0 0 1 .212-1.486c1.735.25 3.261.68 4.376 1.252C21.032 10.04 22 10.859 22 12c0 .76-.44 1.379-.998 1.842-.562.466-1.33.854-2.213 1.164-1.773.623-4.173.994-6.789.994s-5.016-.371-6.79-.994c-.882-.31-1.65-.697-2.212-1.164C2.441 13.379 2 12.76 2 12c0-1.03.794-1.795 1.684-2.316C4.54 9.182 5.686 8.78 7 8.497zM12 3a.75.75 0 0 1 .75.75v8.5a.75.75 0 0 1-1.5 0v-8.5A.75.75 0 0 1 12 3" /></svg>

After

Width:  |  Height:  |  Size: 976 B

View File

@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none">
<g stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M8 3H5.5A2.5 2.5 0 0 0 3 5.5V8M16 3h2.5A2.5 2.5 0 0 1 21 5.5V8M21 16v2.5a2.5 2.5 0 0 1-2.5 2.5H16M8 21H5.5A2.5 2.5 0 0 1 3 18.5V16"/>
<path d="m11 13 5-5a1.77 1.77 0 0 1 2.5 2.5l-5 5M11 13l2.5 2.5M11 13c-1.8-.4-3 .4-3 2 0 1-.6 1.6-1.5 2 1.2.7 3 .7 4.4-.1 1-.6 1.6-1.2 1.6-1.9"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 500 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24"><path fill="currentColor" fill-opacity="0.9" d="M20.5 12c0-4.38-3.766-8-8.5-8s-8.5 3.62-8.5 8c0 1.372.367 2.662 1.014 3.791.482.842.853 1.945.524 3.07q-.076.26-.161.517a16 16 0 0 0 1.241-.205c.922-.19 1.86-.072 2.7.248A9 9 0 0 0 12 20c4.734 0 8.5-3.62 8.5-8m1.5 0c0 5.285-4.517 9.5-10 9.5-1.31 0-2.564-.24-3.714-.677-.615-.233-1.262-.305-1.865-.18a18.5 18.5 0 0 1-2.597.34.75.75 0 0 1-.747-1.016c.196-.51.373-1.018.522-1.527l.027-.106c.116-.537-.047-1.157-.414-1.797A9.1 9.1 0 0 1 2 12c0-5.285 4.517-9.5 10-9.5S22 6.715 22 12" /></svg>

After

Width:  |  Height:  |  Size: 631 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24"><path fill="currentColor" fill-opacity="0.9" d="M17.5 10.45a.75.75 0 0 1 1.062-.682l.166.076a1.563 1.563 0 0 0 1.336-2.826L15.46 4.772c-.736 1.422-2.078 2.176-3.46 2.176s-2.727-.754-3.464-2.176L3.936 7.018a1.564 1.564 0 0 0 1.336 2.826l.166-.076a.75.75 0 0 1 1.062.682v6.8a2.25 2.25 0 0 0 2.25 2.25h6.5a2.25 2.25 0 0 0 2.25-2.25zm1.5 6.8A3.75 3.75 0 0 1 15.25 21h-6.5A3.75 3.75 0 0 1 5 17.25v-5.788A3.064 3.064 0 0 1 3.28 5.67l5.317-2.594a.75.75 0 0 1 1.046.454C10.048 4.854 11.05 5.448 12 5.448s1.951-.594 2.358-1.918a.75.75 0 0 1 1.046-.454L20.72 5.67a3.063 3.063 0 0 1-1.72 5.793z" /></svg>

After

Width:  |  Height:  |  Size: 689 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24"><path fill="currentColor" fill-opacity="0.9" d="M3.75 10.25a1.75 1.75 0 1 1 0 3.5 1.75 1.75 0 0 1 0-3.5m8.25 0a1.75 1.75 0 1 1 0 3.5 1.75 1.75 0 0 1 0-3.5m8.25 0a1.75 1.75 0 1 1 0 3.5 1.75 1.75 0 0 1 0-3.5" /></svg>

After

Width:  |  Height:  |  Size: 311 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24"><path fill="currentColor" d="M5.75 12.368C5.75 14.01 6.71 15 8.34 15c1.45 0 2.427-.862 2.427-2.134v-.35c0-.537-.25-.778-.817-.778H8.895c-.378 0-.58.173-.58.49 0 .321.206.499.58.499h.53v.245c0 .528-.413.883-1.029.883-.791 0-1.226-.528-1.226-1.492v-.697c0-.976.426-1.487 1.24-1.487.554 0 .877.33 1.226.672a.56.56 0 0 0 .421.177c.34 0 .577-.228.577-.562s-.254-.697-.641-.972C9.563 9.177 8.973 9 8.306 9 6.722 9 5.75 10.001 5.75 11.599zM12.39 14.916c.451 0 .705-.27.705-.753V9.815c0-.477-.258-.752-.714-.752s-.71.27-.71.752v4.348c0 .478.262.752.718.752M14.963 14.916c.447 0 .697-.27.697-.753v-1.462h1.807c.335 0 .564-.22.564-.545s-.224-.545-.564-.545H15.66v-1.386h2.026c.327 0 .564-.228.564-.553a.545.545 0 0 0-.564-.566h-2.629c-.533 0-.822.283-.822.798v4.26c0 .473.267.752.728.752" /><path fill="currentColor" d="M19.5 6.75a2.25 2.25 0 0 0-2.25-2.25H6.75A2.25 2.25 0 0 0 4.5 6.75v10.5a2.25 2.25 0 0 0 2.25 2.25h10.5a2.25 2.25 0 0 0 2.25-2.25zm1.5 10.5A3.75 3.75 0 0 1 17.25 21H6.75A3.75 3.75 0 0 1 3 17.25V6.75A3.75 3.75 0 0 1 6.75 3h10.5A3.75 3.75 0 0 1 21 6.75z" /></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24"><path fill="currentColor" fill-opacity="0.9" d="M14.5 11A1.5 1.5 0 0 0 13 9.5H5A1.5 1.5 0 0 0 3.5 11v8A1.5 1.5 0 0 0 5 20.5h8a1.5 1.5 0 0 0 1.5-1.5zm6 2a.75.75 0 0 1 1.5 0 3 3 0 0 1-3 3 .75.75 0 0 1 0-1.5 1.5 1.5 0 0 0 1.5-1.5m0-3V8A.75.75 0 0 1 22 8v2a.75.75 0 0 1-1.5 0M8 5a3 3 0 0 1 3-3 .75.75 0 0 1 0 1.5A1.5 1.5 0 0 0 9.5 5 .75.75 0 0 1 8 5m12.5 0A1.5 1.5 0 0 0 19 3.5.75.75 0 0 1 19 2a3 3 0 0 1 3 3 .75.75 0 0 1-1.5 0M16 2a.75.75 0 0 1 0 1.5h-2A.75.75 0 0 1 14 2zm0 12.5a.75.75 0 0 1 0 1.5v3a3 3 0 0 1-3 3H5a3 3 0 0 1-3-3v-8a3 3 0 0 1 3-3h3a.75.75 0 0 1 1.5 0H13a3 3 0 0 1 3 3z" /></svg>

After

Width:  |  Height:  |  Size: 689 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24"><path fill="currentColor" fill-opacity="0.9" d="M8.5 13.25a2.25 2.25 0 0 1 2.25 2.25v3a2.25 2.25 0 0 1-2.25 2.25h-3a2.25 2.25 0 0 1-2.25-2.25v-3a2.25 2.25 0 0 1 2.25-2.25zm11.5-1a.75.75 0 0 1 .75.75v5.25a2.5 2.5 0 0 1-2.5 2.5H13a.75.75 0 0 1 0-1.5h5.25a1 1 0 0 0 1-1V13a.75.75 0 0 1 .75-.75m-14.5 2.5a.75.75 0 0 0-.75.75v3c0 .414.336.75.75.75h3a.75.75 0 0 0 .75-.75v-3a.75.75 0 0 0-.75-.75zM11 3.25a.75.75 0 0 1 0 1.5H5.75a1 1 0 0 0-1 1V11a.75.75 0 0 1-1.5 0V5.75a2.5 2.5 0 0 1 2.5-2.5zm7.5 0a2.25 2.25 0 0 1 2.25 2.25V9a.75.75 0 0 1-1.5 0V5.81l-4.72 4.72a.75.75 0 1 1-1.06-1.06l4.72-4.72H15a.75.75 0 0 1 0-1.5z" /></svg>

After

Width:  |  Height:  |  Size: 717 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24"><path fill="currentColor" fill-opacity="0.9" d="M12 15.25c3.304 0 6.25 2.217 6.25 5.25a.75.75 0 0 1-1.5 0c0-1.937-1.979-3.75-4.75-3.75s-4.75 1.813-4.75 3.75a.75.75 0 0 1-1.5 0c0-3.033 2.946-5.25 6.25-5.25M4.25 18.5c0 .38.17.719.437.948A6.6 6.6 0 0 0 4.5 21l.001.06A2.75 2.75 0 0 1 2.75 18.5V18h1.5zm17 0c0 1.166-.727 2.16-1.752 2.56L19.5 21c0-.534-.066-1.052-.187-1.552.267-.229.437-.568.437-.948V18h1.5zm-17-2.5h-1.5v-3h1.5zm17 0h-1.5v-3h1.5zM12 6.75a3.75 3.75 0 0 1 1.122 7.328 8 8 0 0 0-2.245 0A3.75 3.75 0 0 1 12 6.75m0 1.5a2.25 2.25 0 1 0 0 4.5 2.25 2.25 0 0 0 0-4.5M4.25 11h-1.5V8h1.5zm17 0h-1.5V8h1.5zM6 4.25h-.5c-.69 0-1.25.56-1.25 1.25V6h-1.5v-.5A2.75 2.75 0 0 1 5.5 2.75H6zm12.5-1.5a2.75 2.75 0 0 1 2.75 2.75V6h-1.5v-.5c0-.69-.56-1.25-1.25-1.25H18v-1.5zM11 4.25H8v-1.5h3zm5 0h-3v-1.5h3z" /></svg>

After

Width:  |  Height:  |  Size: 902 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24"><path fill="currentColor" d="M18.25 3.75A3.75 3.75 0 0 1 22 7.5v9a3.75 3.75 0 0 1-3.75 3.75H5.75A3.75 3.75 0 0 1 2 16.5v-9a3.75 3.75 0 0 1 3.75-3.75zm-12.5 1.5A2.25 2.25 0 0 0 3.5 7.5v9a2.25 2.25 0 0 0 2.25 2.25h12.5a2.25 2.25 0 0 0 2.25-2.25v-9a2.25 2.25 0 0 0-2.25-2.25zm5.073 3.106c.464 0 .737.284.737.772v5.698c0 .484-.273.767-.737.767s-.737-.283-.737-.767v-2.295H6.912v2.295c0 .484-.273.767-.737.767s-.737-.283-.737-.767V9.128c0-.488.273-.772.737-.772s.737.284.737.772v2.187h3.174V9.128c0-.488.273-.772.737-.772m4.6.098c2.114 0 3.36 1.26 3.36 3.496 0 2.237-1.246 3.55-3.36 3.55H13.47c-.464 0-.738-.283-.738-.771V9.22c0-.484.274-.767.738-.767zm-1.216 5.825h1.04c1.318 0 2.031-.8 2.031-2.324 0-1.475-.732-2.285-2.03-2.285h-1.041z" /></svg>

After

Width:  |  Height:  |  Size: 838 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24"><path fill="currentColor" fill-opacity="0.9" d="M18.5 2.5a2.998 2.998 0 0 1 .75 5.901v7.197a2.998 2.998 0 1 1-3.652 3.652H8.402a2.999 2.999 0 1 1-3.652-3.652V8.4A2.998 2.998 0 1 1 8.402 4.75h7.196A3 3 0 0 1 18.5 2.5M5.5 17a1.5 1.5 0 1 0 0 3 1.5 1.5 0 0 0 0-3m13 0a1.5 1.5 0 1 0 0 3 1.5 1.5 0 0 0 0-3M8.402 6.25A3 3 0 0 1 6.25 8.401v7.197a3 3 0 0 1 2.152 2.152h7.196a3 3 0 0 1 2.152-2.152V8.4a3 3 0 0 1-2.152-2.151zM5.5 4a1.5 1.5 0 1 0 0 3 1.5 1.5 0 0 0 0-3m13 0a1.5 1.5 0 1 0 0 3 1.5 1.5 0 0 0 0-3" /></svg>

After

Width:  |  Height:  |  Size: 603 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24"><path fill="currentColor" fill-opacity="0.9" d="M11.75 3a.75.75 0 0 1 0 1.5h-5A2.25 2.25 0 0 0 4.5 6.75v7.513l2.28-2.145a1.75 1.75 0 0 1 2.437.037L12 14.94l.763-.762a1.75 1.75 0 0 1 2.474 0l4.041 4.04c.14-.293.222-.62.222-.967v-5a.75.75 0 0 1 1.5 0v5A3.75 3.75 0 0 1 17.25 21H6.75A3.75 3.75 0 0 1 3 17.25V6.75A3.75 3.75 0 0 1 6.75 3zM8.155 13.216a.25.25 0 0 0-.347-.005L4.5 16.323v.927a2.25 2.25 0 0 0 2.25 2.25h10.5c.347 0 .674-.081.968-.222l-4.041-4.04a.25.25 0 0 0-.315-.033l-.039.032-1.293 1.293a.75.75 0 0 1-1.06 0zM18 2c.241 0 .457.148.544.373l.696 1.813a1 1 0 0 0 .575.574l1.812.696a.583.583 0 0 1 0 1.088l-1.812.696a1 1 0 0 0-.575.574l-.696 1.813a.583.583 0 0 1-1.088 0l-.696-1.813a1 1 0 0 0-.575-.574l-1.812-.696a.583.583 0 0 1 0-1.088l1.813-.696a1 1 0 0 0 .574-.574l.696-1.813A.58.58 0 0 1 18 2" /></svg>

After

Width:  |  Height:  |  Size: 910 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24"><path fill="currentColor" fill-opacity="0.9" fill-rule="evenodd" d="M12.089 4.59a.25.25 0 0 0-.18 0L3.683 7.766a.25.25 0 0 0 0 .466l8.226 3.178a.25.25 0 0 0 .18 0l8.225-3.178a.25.25 0 0 0 0-.466zm-.721-1.4a1.75 1.75 0 0 1 1.261 0l8.226 3.178c1.492.576 1.492 2.688 0 3.265l-8.226 3.178a1.75 1.75 0 0 1-1.261 0L3.143 9.633c-1.493-.577-1.493-2.689 0-3.265z" clip-rule="evenodd" /><path fill="currentColor" fill-opacity="0.9" d="m20.855 13.633-8.226 3.178a1.75 1.75 0 0 1-1.261 0l-8.225-3.178c-.89-.344-1.249-1.233-1.078-2.017l9.801 3.787a.37.37 0 0 0 .268 0l9.799-3.786c.17.784-.19 1.672-1.078 2.016" /><path fill="currentColor" fill-opacity="0.9" d="M20.855 17.633 12.63 20.81a1.75 1.75 0 0 1-1.261 0l-8.226-3.178c-.889-.344-1.248-1.233-1.078-2.017l9.802 3.787a.37.37 0 0 0 .267 0l9.8-3.786c.17.784-.19 1.672-1.079 2.016" /></svg>

After

Width:  |  Height:  |  Size: 924 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24"><path fill="currentColor" fill-opacity="0.9" d="M20.127 9.647c0-.764-.006-1.166-.047-1.448l-7.33 4.234v8.466c.263-.106.613-.302 1.274-.684l4.078-2.353c.918-.53 1.227-.72 1.447-.963.21-.235.37-.511.468-.81.101-.313.11-.675.11-1.735zm-7.66-6.634a2.25 2.25 0 0 0-.935 0c-.32.068-.64.242-1.557.772L5.899 6.14c-.661.381-1.007.586-1.231.762l7.331 4.233 7.331-4.233c-.223-.176-.568-.381-1.228-.762l-4.078-2.354c-.917-.53-1.235-.704-1.556-.772m-8.594 11.34c0 1.06.01 1.423.11 1.735.098.3.257.576.468.81.22.244.53.434 1.448.964l4.076 2.353c.66.381 1.01.577 1.274.683v-8.465L3.918 8.199c-.04.282-.045.684-.045 1.448zm17.754 0c0 .96.009 1.607-.184 2.199-.162.5-.428.96-.78 1.35-.416.463-.981.78-1.811 1.258l-4.078 2.354c-.83.48-1.386.811-1.995.94-.514.11-1.045.11-1.56 0-.455-.097-.882-.308-1.417-.609l-.577-.331-4.076-2.354c-.831-.48-1.397-.795-1.813-1.257a3.75 3.75 0 0 1-.78-1.351c-.192-.592-.183-1.24-.183-2.198V9.647c0-.96-.008-1.607.184-2.198.162-.5.427-.96.779-1.351.416-.462.982-.778 1.813-1.258l4.076-2.353c.83-.48 1.386-.811 1.995-.94a3.75 3.75 0 0 1 1.56 0c.608.129 1.164.46 1.995.94l4.077 2.353c.83.48 1.395.796 1.811 1.258.352.39.618.85.78 1.35.193.592.184 1.24.184 2.199z" /></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24"><path fill="currentColor" fill-opacity="0.9" d="M12.916 1.995c1.993 0 3.35 1.678 4.15 3.27.829 1.65 1.274 3.612 1.274 4.903 0 1.191-.15 2.356-.432 3.463a5.88 5.88 0 0 1 3.386 2.8c.484.894.729 1.897.71 2.913a.843.843 0 1 1-1.687-.032 4.2 4.2 0 0 0-2.93-4.077c-1.56 3.92-4.948 6.77-9.5 6.77-1.07 0-2.439.004-3.54-.251-.553-.129-1.128-.341-1.576-.725a2.17 2.17 0 0 1-.776-1.7c0-.45.086-.877.317-1.253.233-.377.563-.62.9-.79.325-.164.698-.279 1.061-.379.186-.05.386-.102.587-.154q.306-.078.638-.17l.003-.001c2.268-.625 4.03-2.01 5.086-3.749a4.7 4.7 0 0 0-.936-.227c-1.435-.197-3.08.196-4.561 1.042a.844.844 0 0 1-.838-1.465c1.725-.986 3.745-1.508 5.63-1.248q.735.1 1.422.365a7.35 7.35 0 0 0 .03-4.638l-.001-.004c-.29-.913-.48-1.945-.364-2.8.059-.433.208-.902.544-1.273.36-.397.853-.59 1.403-.59m-.883 11.71c-1.284 2.109-3.412 3.767-6.083 4.503q-.358.1-.667.179c-.208.054-.392.1-.563.147-.35.096-.584.176-.75.259-.153.077-.2.136-.221.169-.022.035-.066.13-.066.367 0 .203.063.316.183.418.154.132.43.263.863.363.873.203 2.03.207 3.158.207 3.53 0 6.328-2.111 7.747-5.247a4.2 4.2 0 0 0-1.418.429l-.758.387-.38-.761a4.9 4.9 0 0 0-1.045-1.42m.883-10.022c-.129 0-.15.032-.153.036-.025.027-.089.124-.121.366-.067.493.04 1.242.298 2.059l.143.486a9 9 0 0 1-.285 5.513c.522.407.993.916 1.393 1.537a5.9 5.9 0 0 1 2.051-.325c.267-1 .41-2.072.41-3.187 0-1-.37-2.703-1.093-4.145-.753-1.499-1.687-2.34-2.643-2.34" /></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24"><path fill="currentColor" fill-opacity="0.9" d="M22.56 19.592h-1.965L12.008 6.564 4.494 17.94h10.668l1.09 1.652H1.44L12.009 3.59zm-3.138 0h-1.93l-4.988-7.662h1.915zm-7.232-3.484H8.54l1.824-2.74zm10.37-4.442h-3.749l1.875-2.807z" /></svg>

After

Width:  |  Height:  |  Size: 332 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24"><path fill="currentColor" fill-opacity="0.9" d="M9.796 9.348V7.495c0-.156.059-.273.195-.35L13.716 5c.507-.293 1.112-.43 1.736-.43 2.34 0 3.822 1.814 3.822 3.745 0 .136 0 .292-.02.448l-3.861-2.262a.65.65 0 0 0-.702 0zm8.698 7.215v-4.427a.65.65 0 0 0-.352-.604l-4.894-2.847 1.599-.917a.36.36 0 0 1 .39 0l3.725 2.145c1.072.624 1.794 1.95 1.794 3.237 0 1.482-.878 2.848-2.262 3.413m-9.848-3.9-1.599-.936c-.136-.078-.195-.195-.195-.35v-4.29C6.852 5 8.451 3.42 10.616 3.42c.819 0 1.579.273 2.223.76L8.997 6.405a.65.65 0 0 0-.35.604zm3.442 1.989-2.292-1.287v-2.73l2.292-1.287 2.29 1.287v2.73zm1.472 5.928a3.65 3.65 0 0 1-2.223-.76l3.841-2.224a.65.65 0 0 0 .351-.604v-5.655l1.619.936c.137.078.195.195.195.35v4.29c0 2.087-1.619 3.667-3.783 3.667m-4.622-4.348-3.724-2.145c-1.073-.624-1.794-1.95-1.794-3.237 0-1.502.897-2.847 2.281-3.413v4.446c0 .273.117.468.351.605l4.875 2.827-1.599.917a.35.35 0 0 1-.39 0m-.214 3.198c-2.203 0-3.822-1.658-3.822-3.705 0-.156.02-.312.039-.468l3.841 2.223a.65.65 0 0 0 .702 0l4.895-2.828v1.853c0 .156-.059.273-.195.35l-3.725 2.146c-.507.292-1.111.429-1.735.429m4.836 2.32a4.876 4.876 0 0 0 4.778-3.9c2.183-.566 3.588-2.613 3.588-4.7a4.93 4.93 0 0 0-1.638-3.646c.097-.41.156-.82.156-1.228 0-2.789-2.262-4.875-4.875-4.875-.527 0-1.034.077-1.54.253a4.89 4.89 0 0 0-3.413-1.404 4.876 4.876 0 0 0-4.778 3.9c-2.184.566-3.588 2.613-3.588 4.7 0 1.365.585 2.69 1.638 3.646-.098.41-.156.819-.156 1.228 0 2.789 2.262 4.875 4.875 4.875.526 0 1.033-.077 1.54-.253a4.89 4.89 0 0 0 3.413 1.404" /></svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24"><path fill="currentColor" fill-opacity="0.9" d="M14.069 2.016a5.63 5.63 0 0 1 5.578 4.056 3 3 0 0 1 1.105.699 2.97 2.97 0 0 1-.093 4.289 2.972 2.972 0 0 1-.19 4.54l-.033.024.019.029c.343.53.514 1.165.47 1.817l-.008.097a2.97 2.97 0 0 1-2.744 2.665 3 3 0 0 1-1.356-.22 2.97 2.97 0 0 1-5.143.853c-.45.216-.94.336-1.439.35a1 1 0 0 1-.11.007h-.79l-.059-.003c-.982-.059-.962-1.562.059-1.562h.708l.04-.003.042-.001a2.025 2.025 0 0 0 .069-4.048l-.07.004h-6.27a.784.784 0 0 1-.554-1.335.78.78 0 0 1 .553-.23h6.19l.04-.004h.043a2.024 2.024 0 0 0 .447-3.998l-.123-.028a2 2 0 0 0-.264-.04l-.061.002H3.853a.785.785 0 0 1-.808-.782.78.78 0 0 1 .808-.781h6.19l.042-.003.04-.001a2.024 2.024 0 0 0 .077-4.05l-.076.005h-.79l-.06-.001c-.982-.06-.962-1.561.059-1.562h.705a1 1 0 0 1 .085-.005c.353 0 .695.05 1.018.146l.027-.017a5.64 5.64 0 0 1 2.899-.909m.286 10.688c-.217 0-.431.05-.626.147l-.06.032-.073.045a3.6 3.6 0 0 1-1.235 1.894 3.58 3.58 0 0 1 1.353 2.806 3.57 3.57 0 0 1-.81 2.27 1.408 1.408 0 0 0 2.484-1.163l-.025-.11-.013-.069-.004-.049v-.087l.004-.048.013-.069.015-.062.02-.057.03-.07.005-.008a.8.8 0 0 1 .138-.192l.05-.044.036-.029.043-.031.037-.023.049-.026.064-.026.05-.019.034-.01.069-.013.064-.008h.118l.052.006.049.008.054.013.027.01.043.014.049.02.038.019.047.027.03.02q.079.055.143.13l.035.04.029.043a1.41 1.41 0 0 0 1.578.587 1.411 1.411 0 0 0 .534-2.41 3 3 0 0 1-.416.007l-.05-.005-.023-.005h-.015a2.97 2.97 0 0 1-2.626-2.258l-.023-.103a1.41 1.41 0 0 0-1.385-1.144m-4.513 4.191a.784.784 0 0 1 .748 1.087.78.78 0 0 1-.748.477H6.608a.783.783 0 0 1 0-1.564zM18.8 7.472a1.403 1.403 0 0 0-1.554 1.324l-.002.075a.784.784 0 0 1-.73.78l-.052.003a.783.783 0 0 1-.782-.73V8.87a1.41 1.41 0 0 0-1.51-1.403 1.4 1.4 0 0 0-.715.258A3.6 3.6 0 0 1 12.349 9.2a3.6 3.6 0 0 1 1.277 2.032 2.97 2.97 0 0 1 2.286.346 2.97 2.97 0 0 1 1.344 1.882l.018.082a1.41 1.41 0 0 0 1.227 1.108l.11.009.036.002q.08 0 .16-.01l.074-.009.054-.014a1.408 1.408 0 0 0-.216-2.783l-.069-.002a.78.78 0 0 1-.601-.28l-.03-.04a.78.78 0 0 1 .115-1.049.8.8 0 0 1 .46-.193h.056a1.41 1.41 0 0 0 1.381-1.68 1.4 1.4 0 0 0-.383-.722 1.4 1.4 0 0 0-.691-.375l-.091-.016zm-8.949 3.77a.784.784 0 1 1 0 1.566H2.165a.784.784 0 0 1-.536-1.326.78.78 0 0 1 .536-.238H9.85zm-.008-5.66a.787.787 0 0 1 .747 1.088.79.79 0 0 1-.44.424.8.8 0 0 1-.307.052H6.609a.784.784 0 0 1-.747-1.087.785.785 0 0 1 .747-.477zm5.931-1.705a4.07 4.07 0 0 0-3.03-.014l-.046.018c.55.565.898 1.295.99 2.077a2.97 2.97 0 0 1 2.774.904 2.96 2.96 0 0 1 1.49-.88 4.07 4.07 0 0 0-2.178-2.105" /></svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24"><path fill="currentColor" fill-opacity="0.9" fill-rule="evenodd" d="m12.393 22-8.66-5.002 8.66-5 8.66 5-8.66 5z" clip-rule="evenodd" opacity="0.73" /><path fill="currentColor" fill-opacity="0.9" fill-rule="evenodd" d="m3.732 7 8.66-5v20l-8.66-5z" clip-rule="evenodd" opacity="0.73" /></svg>

After

Width:  |  Height:  |  Size: 386 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24"><g clip-path="url(#midjourney-s_svg__a)"><path fill="currentColor" d="M22.127 16.94a.51.51 0 0 1 .502.309.51.51 0 0 1-.126.575c-1.33 1.208-3.018 2.281-5.038 3.278.129.053.251.08.379.08.18 0 .349-.048.538-.155l.018-.01.005-.004.018-.01c.126-.076.243-.163.465-.347l.178-.147c.281-.235.44-.354.653-.477a2.07 2.07 0 0 1 1.049-.292c.376 0 .718.103 1.048.292l.005.003.018.012c.203.117.36.237.63.462l.179.147c.218.182.334.269.457.343h.002l.022.014.017.01h.002c.186.107.351.156.524.16h.019a.51.51 0 0 1 .37.133.51.51 0 0 1 .122.58.52.52 0 0 1-.3.277.5.5 0 0 1-.192.025v.002a2.1 2.1 0 0 1-1.147-.35v-.002l-.017-.01-.022-.013-.014-.008-.017-.014a6 6 0 0 1-.509-.39l-.155-.13-.004-.002a4 4 0 0 0-.458-.342l-.027-.016-.005-.003-.012-.006a1.06 1.06 0 0 0-.538-.156c-.18 0-.35.05-.54.158l-.003.002-.02.01-.01.006-.001.001a3.6 3.6 0 0 0-.468.348l-.157.133-.001-.001q-.25.218-.527.403l-.004.003-.021.014-.023.013-.003.003-.09.054c-.318.19-.682.293-1.052.295h-.002c-.376 0-.718-.104-1.048-.292v-.001l-.097-.058-.017-.011-.023-.013-.007-.004-.026-.018-.003-.002q-.221-.151-.428-.326a.5.5 0 0 1-.146-.122l-.084-.07-.005-.002a4 4 0 0 0-.457-.342l-.024-.014-.01-.005-.01-.006a1.06 1.06 0 0 0-.539-.156c-.178 0-.347.049-.535.155l-.019.01-.003.004-.019.01h-.001a4 4 0 0 0-.464.346h.001l-.158.133-.001-.001q-.251.218-.526.403l-.007.005-.02.01v.002l-.022.013-.004.003-.091.054c-.317.19-.68.293-1.05.295-.377 0-.72-.104-1.05-.292v-.001l-.096-.058h-.002l-.016-.01-.022-.014-.01-.005-.02-.015h-.003a6 6 0 0 1-.504-.39l-.159-.131-.003-.004a4 4 0 0 0-.456-.339l-.021-.014-.018-.01-.008-.003a1.06 1.06 0 0 0-.539-.156c-.179 0-.349.05-.54.158v-.001l-.017.012-.001-.002-.013.01-.002.001a2 2 0 0 0-.197.132l-.27.216-.158.132v-.001q-.251.218-.527.403l-.004.003-.023.014-.021.013-.005.003-.09.054c-.33.197-.708.299-1.092.293h-.01l-.011-.001c-.792-.096-1.4-.407-1.995-.913l-.035-.03-.002-.001c-.05-.043-.122-.11-.196-.178l-.196-.18v-.002l-.106-.098-.011-.01-.007-.006a1.3 1.3 0 0 0-.288-.219l-.006-.002a1.6 1.6 0 0 0-.258.08.5.5 0 0 1-.076.056l.001.001-.008.005-.006.004-.008.004a.5.5 0 0 1-.147.046 5.5 5.5 0 0 0-.72.484l-.001.002-.416.324H1.61l-.054.042-.001.001-.334.243c-.315.214-.604.35-.912.35H.297a.512.512 0 0 1-.349-.85.5.5 0 0 1 .332-.167h.03l.006-.002q.006 0 .02-.004a1 1 0 0 0 .069-.026q.088-.038.231-.133l.039-.027c.073-.051.156-.112.259-.19l.386-.301.06-.047v-.001c.21-.16.403-.3.588-.419l-.546-1.045a.513.513 0 0 1 .18-.667.5.5 0 0 1 .238-.076zm-19.44 2.269.205.39q.124-.035.252-.057l.085-.008h.003c.164 0 .321.04.47.112.135.064.258.149.393.261l.14.122.003.002.107.1.143.133.096.09h.001c.064.06.122.111.154.139h.001c.459.396.884.614 1.437.687.175-.006.345-.055.496-.143l.009-.005.018-.01.014-.009.014-.007.002-.001a3.6 3.6 0 0 0 .467-.349l.177-.147c.282-.235.44-.355.654-.477.33-.189.672-.292 1.05-.292.328 0 .63.08.922.226l.125.066.004.003.019.012c.1.058.192.118.29.19l.341.272.176.147c.22.183.336.268.46.342l.004.003.025.015.007.004.007.003c.19.11.361.16.541.16a1 1 0 0 0 .403-.088l.14-.07.017-.012.014-.007.002-.001a3.6 3.6 0 0 0 .467-.349l.177-.147v-.001c.283-.234.442-.354.655-.476.33-.189.672-.292 1.048-.292.33 0 .632.08.924.226l.125.066.003.003.023.014.146.09c.117.077.23.166.382.29 1.573-.747 2.935-1.53 4.079-2.372zm.73-17.395a.5.5 0 0 1 .266.012h.01l.038.017c2.449 1.046 5.09 2.945 7.217 5.28 2.326 2.554 3.94 5.493 4.573 8.624l.113.629a.52.52 0 0 1-.187.477.51.51 0 0 1-.505.074c-2.177-.875-4.211-1.199-6.115-1.043l-.379.037c-1.706.196-3.278.777-4.734 1.674h.001c-.482.299-1.022-.299-.668-.748 1.824-2.314 2.535-4.898 2.3-7.653-.191-2.256-1.014-4.546-2.255-6.624h-.001a.51.51 0 0 1 .057-.595.5.5 0 0 1 .27-.16m6.243 3.35c-.479-.33-.116-1.074.439-.905 1.828.558 3.97 2.147 5.872 4.301 1.875 2.125 3.385 4.64 4.23 7.155l.159.503a.51.51 0 0 1-.66.625l-.007-.002-.008-.004-.014-.006-.386-.154q-.142-.057-.286-.11l-.014-.007c-.645-.234-1.124-.274-1.828-.094a.51.51 0 0 1-.58-.266l-.037-.093c-1.348-4.904-3.633-8.702-6.88-10.942M4.736 3.468C5.618 5.27 6.2 7.19 6.364 9.107c.2 2.358-.24 4.615-1.4 6.698a12.1 12.1 0 0 1 3.313-.89h.002l.053-.006c1.934-.225 3.976.01 6.127.752-.585-2.614-1.903-5.098-3.793-7.317l-.401-.458-.07-.079c-1.627-1.785-3.567-3.3-5.46-4.34m9.012 4.277c1.575 2.095 2.786 4.676 3.647 7.636a3.6 3.6 0 0 1 1.65.127c-.815-2.174-2.147-4.343-3.78-6.208l-.057-.065a20 20 0 0 0-1.46-1.49" /></g><defs><clipPath id="midjourney-s_svg__a"><path fill="#fff" d="M0 0h24v24H0z" /></clipPath></defs></svg>

After

Width:  |  Height:  |  Size: 4.4 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24"><path fill="currentColor" fill-opacity="0.9" fill-rule="evenodd" d="M10.474 2.5c4.23 0 7.657 3.061 7.657 6.836v.003c0 3.762-3.408 6.817-7.619 6.834h7.768l2.988 5.327h-7.796l-2.99-5.327-.005.001V21.5H2.71L5.429 12h2.6a16.5 16.5 0 0 1-.21-2.664c0-3.775 1.19-6.836 2.655-6.836m0 1.251c-.767 0-1.388 2.502-1.388 5.585s.62 5.584 1.389 5.584c.767 0 1.388-2.5 1.388-5.584 0-3.082-.62-5.584-1.389-5.585" clip-rule="evenodd" /></svg>

After

Width:  |  Height:  |  Size: 520 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24"><path fill="currentColor" fill-opacity="0.9" d="M21.686 4.427v15.116l-3.344.836V3.522zM10.888 11.95v7.174l-3.344.906v-8.916zM5.595 5.263v13.444l-3.275.836V4.428zm10.796 12.54-3.343-.837V9.791l3.343-.835z" /></svg>

After

Width:  |  Height:  |  Size: 309 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24"><path fill="currentColor" fill-opacity="0.9" d="M11.25 15v-2.25H9a.75.75 0 0 1 0-1.5h2.25V9a.75.75 0 0 1 1.5 0v2.25H15a.75.75 0 0 1 0 1.5h-2.25V15a.75.75 0 0 1-1.5 0m0-9.25V4.5h-4.5A2.25 2.25 0 0 0 4.5 6.75v4.5h1.25a.75.75 0 0 1 0 1.5H4.5v4.5a2.25 2.25 0 0 0 2.25 2.25h4.5v-1.25a.75.75 0 0 1 1.5 0v1.25h4.5a2.25 2.25 0 0 0 2.25-2.25v-4.5h-1.25a.75.75 0 0 1 0-1.5h1.25v-4.5a2.25 2.25 0 0 0-2.25-2.25h-4.5v1.25a.75.75 0 0 1-1.5 0M21 17.25A3.75 3.75 0 0 1 17.25 21H6.75A3.75 3.75 0 0 1 3 17.25V6.75A3.75 3.75 0 0 1 6.75 3h10.5A3.75 3.75 0 0 1 21 6.75z" /></svg>

After

Width:  |  Height:  |  Size: 654 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24"><path fill="#E60024" d="M22 12c0 5.523-4.477 10-10 10S2 17.523 2 12 6.477 2 12 2s10 4.477 10 10" /><path fill="#fff" d="M9.239 21.609a10 10 0 0 1-.886-.298c-.084-.79-.163-2.006.038-2.867.182-.782 1.17-4.968 1.17-4.968-.007-.013-.3-.606-.3-1.484 0-1.39.808-2.428 1.812-2.428.854 0 1.266.642 1.266 1.408 0 .859-.544 2.14-.827 3.33-.238.996.5 1.807 1.48 1.807 1.778 0 3.145-1.875 3.145-4.58 0-2.395-1.722-4.07-4.177-4.07-2.847 0-4.516 2.134-4.516 4.34 0 .858.33 1.777.738 2.281a.3.3 0 0 1 .068.286c-.077.315-.246.997-.278 1.135-.045.18-.145.22-.334.132-1.246-.58-2.03-2.407-2.03-3.875 0-3.157 2.295-6.052 6.61-6.052 3.467 0 6.165 2.471 6.166 5.774 0 3.447-2.175 6.222-5.19 6.222-1.013 0-1.964-.529-2.29-1.15-.005.018-.501 1.914-.621 2.38-.226.867-.836 1.955-1.243 2.62q.1.03.2.057m-2.851-1.332q.065.044.132.086-.067-.042-.132-.085m3.302 1.45" /></svg>

After

Width:  |  Height:  |  Size: 944 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24"><path fill="currentColor" fill-opacity="0.9" d="M11.25 20v-7.25H4a.75.75 0 0 1 0-1.5h7.25V4a.75.75 0 0 1 1.5 0v7.25H20a.75.75 0 0 1 0 1.5h-7.25V20a.75.75 0 0 1-1.5 0" /></svg>

After

Width:  |  Height:  |  Size: 271 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24"><path fill="currentColor" fill-opacity="0.9" d="M15.358 3.463a.78.78 0 0 0-.45-.88.88.88 0 0 0-1.02.197L4.6 13.072a.75.75 0 0 0-.122.844.85.85 0 0 0 .766.459h4.859l-1.46 6.162a.78.78 0 0 0 .448.88.88.88 0 0 0 1.02-.197l9.29-10.292a.75.75 0 0 0 .12-.844.85.85 0 0 0-.765-.459h-4.859z" /></svg>

After

Width:  |  Height:  |  Size: 388 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24"><path fill="currentColor" fill-opacity="0.9" d="M17.25 3A3.75 3.75 0 0 1 21 6.75v10.5A3.75 3.75 0 0 1 17.25 21H6.75A3.75 3.75 0 0 1 3 17.25V6.75A3.75 3.75 0 0 1 6.75 3zM8.155 13.217a.25.25 0 0 0-.347-.006L4.5 16.323v.927a2.25 2.25 0 0 0 2.25 2.25h10.5c.347 0 .674-.081.968-.222l-4.041-4.04a.25.25 0 0 0-.354 0L12.53 16.53a.75.75 0 0 1-1.06 0zM6.75 4.5A2.25 2.25 0 0 0 4.5 6.75v7.514l2.28-2.145a1.75 1.75 0 0 1 2.437.036L12 14.94l.763-.762a1.75 1.75 0 0 1 2.474 0l4.041 4.04c.14-.293.222-.62.222-.967V6.75a2.25 2.25 0 0 0-2.25-2.25zm8.25 3a1.5 1.5 0 1 1 0 3 1.5 1.5 0 0 1 0-3" /></svg>

After

Width:  |  Height:  |  Size: 680 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24"><path fill="currentColor" fill-opacity="0.9" d="M8.518 3H9v18h-.482c-.813 0-1.469 0-2-.043-.546-.045-1.026-.14-1.47-.366a3.75 3.75 0 0 1-1.64-1.639c-.226-.444-.32-.924-.365-1.47C3 16.95 3 16.295 3 15.482V8.518c0-.813 0-1.469.043-2 .045-.546.14-1.026.366-1.47a3.75 3.75 0 0 1 1.639-1.64c.444-.226.924-.32 1.47-.365C7.05 3 7.706 3 8.519 3M10.5 21h4.982c.813 0 1.469 0 2-.043.546-.045 1.026-.14 1.47-.366a3.75 3.75 0 0 0 1.64-1.639c.226-.444.32-.924.365-1.47.043-.531.043-1.187.043-2V12.75H10.5zM21 11.25V8.518c0-.813 0-1.469-.043-2-.045-.546-.14-1.026-.366-1.47a3.75 3.75 0 0 0-1.639-1.64c-.444-.226-.924-.32-1.47-.365C16.95 3 16.295 3 15.482 3H10.5v8.25z" /></svg>

After

Width:  |  Height:  |  Size: 759 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24"><path fill="currentColor" fill-opacity="0.9" fill-rule="evenodd" d="M18.25 3A2.77 2.77 0 0 1 21 5.75v1.5A2.75 2.75 0 0 1 18.25 10H17v8.25A2.75 2.75 0 0 1 14.25 21H4.783A2.783 2.783 0 0 1 2 18.218v-.468A2.75 2.75 0 0 1 4.75 15H5V5.746A2.75 2.75 0 0 1 7.75 3zM17 5.75V8.5h1.25c.69 0 1.25-.56 1.25-1.25v-1.5a1.27 1.27 0 0 0-1.25-1.25A1.25 1.25 0 0 0 17 5.75m-5.5 11.73a.98.98 0 0 0-.982-.98H4.75c-.69 0-1.25.56-1.25 1.25v.468A1.28 1.28 0 0 0 4.783 19.5H11.8c-.192-.375-.3-.8-.3-1.25zM9 7.75A.75.75 0 0 1 9.75 7h2a.75.75 0 0 1 0 1.5h-2A.75.75 0 0 1 9 7.75M9.75 10a.75.75 0 0 0 0 1.5h1a.75.75 0 0 0 0-1.5z" clip-rule="evenodd" /></svg>

After

Width:  |  Height:  |  Size: 726 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24"><path fill="currentColor" fill-opacity="0.9" d="M19.25 3A2.77 2.77 0 0 1 22 5.75v1.5A2.75 2.75 0 0 1 19.25 10H18v8.252A2.75 2.75 0 0 1 15.252 21H5.783A2.784 2.784 0 0 1 3 18.218v-.468A2.75 2.75 0 0 1 5.75 15H6V5.746A2.744 2.744 0 0 1 8.75 3zM5.75 16.5c-.69 0-1.25.56-1.25 1.25v.468A1.28 1.28 0 0 0 5.783 19.5h7.02a2.7 2.7 0 0 1-.303-1.248v-.502c0-.69-.56-1.25-1.25-1.25zm3-12c-.333 0-.65.131-.884.364v.001c-.235.234-.366.55-.366.881V15h3.75A2.75 2.75 0 0 1 14 17.75v.502c0 .689.56 1.248 1.248 1.248h.004c.689 0 1.248-.56 1.248-1.248V5.75c0-.439.107-.866.303-1.25zm3.5 5.5a.75.75 0 0 1 0 1.5h-1.5a.75.75 0 0 1 0-1.5zm1-3a.75.75 0 0 1 0 1.5h-2.5a.75.75 0 0 1 0-1.5zm6-2.5A1.25 1.25 0 0 0 18 5.75V8.5h1.25c.69 0 1.25-.56 1.25-1.25v-1.5a1.27 1.27 0 0 0-1.25-1.25" /></svg>

After

Width:  |  Height:  |  Size: 864 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 18 18"><path fill="currentColor" d="M15.375 8.813c0-.932-.755-1.688-1.687-1.688H8.811c-.931 0-1.687.756-1.687 1.688v4.874c0 .933.756 1.688 1.688 1.688h4.874c.933 0 1.688-.755 1.688-1.687zm-5.332.593c.18-.098.4-.09.574.021l2.1 1.35a.562.562 0 0 1 0 .946l-2.1 1.35a.562.562 0 0 1-.867-.473V9.9c0-.206.112-.395.293-.494m-4.618-.613a1.69 1.69 0 0 0-1.86-.008l-.927.607c.101.835.812 1.483 1.675 1.483H6V9.174q-.015-.008-.028-.017zM4.688 3.75a.937.937 0 1 1 0 1.875.937.937 0 0 1 0-1.875m6.187.563c0-.932-.755-1.688-1.687-1.688H4.312c-.931 0-1.687.756-1.687 1.688v3.743l.323-.212a2.81 2.81 0 0 1 3.101.013l.096.063A2.81 2.81 0 0 1 8.813 6h2.062zM12 6h1.688A2.813 2.813 0 0 1 16.5 8.813v4.874a2.813 2.813 0 0 1-2.812 2.813H8.811A2.813 2.813 0 0 1 6 13.688V12H4.313A2.813 2.813 0 0 1 1.5 9.188V4.312A2.813 2.813 0 0 1 4.313 1.5h4.875A2.813 2.813 0 0 1 12 4.313z" /></svg>

After

Width:  |  Height:  |  Size: 952 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none"><path fill="currentColor" d="M18.47 4.47a.75.75 0 1 1 1.06 1.06L13.06 12l6.47 6.47a.75.75 0 1 1-1.06 1.06L12 13.06l-6.47 6.47a.75.75 0 1 1-1.06-1.06L10.94 12 4.47 5.53a.75.75 0 1 1 1.06-1.06L12 10.94z"/></svg>

After

Width:  |  Height:  |  Size: 282 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none"><path fill="currentColor" d="m4.01 12.321 3.304 3.304L3.22 19.72a.75.75 0 1 0 1.06 1.06l4.095-4.094 3.304 3.303c1.593 1.593 4.32.7 4.662-1.526l.74-4.807a1.25 1.25 0 0 1 .694-.936l2.755-1.322a2.75 2.75 0 0 0 .755-4.424l-4.259-4.258a2.75 2.75 0 0 0-4.424.754L11.28 6.225a1.25 1.25 0 0 1-.936.694l-4.807.74C3.31 8 2.417 10.729 4.01 12.32"/></svg>

After

Width:  |  Height:  |  Size: 416 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none"><path fill="currentColor" d="M12.603 3.47a2.75 2.75 0 0 1 4.423-.755l4.258 4.26a2.75 2.75 0 0 1-.754 4.423l-2.755 1.321a1.25 1.25 0 0 0-.694.938l-.74 4.806c-.342 2.227-3.07 3.12-4.662 1.527l-3.304-3.304-4.095 4.095a.75.75 0 0 1-1.06-1.062l4.093-4.095-3.302-3.303C2.418 10.728 3.31 8 5.537 7.659l4.807-.74a1.25 1.25 0 0 0 .936-.695zm3.363.306a1.25 1.25 0 0 0-2.011.343l-1.322 2.755a2.75 2.75 0 0 1-2.062 1.528l-4.806.74a1.25 1.25 0 0 0-.694 2.118l3.75 3.75a.8.8 0 0 1 .167.167l3.751 3.752c.724.724 1.964.318 2.12-.694l.74-4.807a2.75 2.75 0 0 1 1.527-2.06l2.755-1.323a1.25 1.25 0 0 0 .343-2.01z"/></svg>

After

Width:  |  Height:  |  Size: 674 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 176 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none"><path fill="currentColor" d="M19.52 12a7.5 7.5 0 0 0-7.5-7.5c-1.462 0-2.622.351-3.655.985-.72.442-1.39 1.032-2.068 1.765H8.75a.75.75 0 0 1 0 1.5h-4A.75.75 0 0 1 4 8V4a.75.75 0 0 1 1.5 0v1.91c.652-.667 1.333-1.245 2.081-1.704C8.858 3.422 10.291 3 12.021 3a9 9 0 1 1-8.488 12 .75.75 0 1 1 1.414-.5A7.5 7.5 0 0 0 19.52 12"/></svg>

After

Width:  |  Height:  |  Size: 400 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 18 18"><path fill="currentColor" d="M12.068 2.698a2.288 2.288 0 0 1 3.238 3.234l-1.502 1.504c1.01 1.24 1.704 2.49 1.942 3.65.264 1.286-.036 2.485-1.086 3.334-.858.694-1.824.608-2.624.274-.782-.326-1.486-.917-1.977-1.396a.562.562 0 1 1 .785-.805c.456.445 1.039.918 1.626 1.163.57.238 1.056.233 1.482-.11.663-.537.888-1.277.692-2.234-.184-.893-.737-1.947-1.64-3.075L11.57 9.674a2.81 2.81 0 0 1-1.99.826H8.062a.563.563 0 0 1-.562-.563V8.432c0-.746.296-1.462.824-1.99L9.717 5.05c-1.081-.622-2.226-.945-3.267-.975-1.196-.033-2.217.319-2.887.996-.814.823-1.114 2.101-.84 3.582.274 1.477 1.115 3.093 2.48 4.474 1.622 1.638 3.112 2.086 3.825 2.266a.562.562 0 1 1-.275 1.09c-.835-.21-2.536-.733-4.349-2.565-1.504-1.521-2.467-3.335-2.787-5.06-.319-1.72-.004-3.416 1.146-4.578.943-.953 2.297-1.37 3.72-1.329 1.329.037 2.752.472 4.053 1.28zM8.625 9.375h.955c.448 0 .878-.178 1.194-.495l3.736-3.743a1.162 1.162 0 0 0-1.646-1.644L9.119 7.238a1.7 1.7 0 0 0-.494 1.194z" /></svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -0,0 +1,218 @@
import { expect, test } from "@playwright/test";
const storageKey = "canvas.image-toolbar.v1";
test("image toolbar customization preserves actions, saves preferences and respects enabled tools", async ({
page,
context,
}) => {
const errors: string[] = [];
page.on("pageerror", (error) => errors.push(error.message));
await context.addInitScript(() => localStorage.setItem("theme", "light"));
const settings = {
canvasEnabled: true,
agentEnabled: true,
layerSplitEnabled: true,
angleEditEnabled: true,
membershipEnabled: false,
canvasToolbarConfig: {} as Record<string, unknown>,
};
await context.route("**/api/**", async (route) => {
const path = new URL(route.request().url()).pathname;
let data: unknown = {};
if (path.includes("validate-session"))
data = { valid: true, success: true };
else if (path === "/api/auth/session")
data = {
user: { id: "toolbar-test-user", name: "设计师", type: "admin" },
expires: "2099-01-01",
};
else if (path === "/api/site-settings") data = settings;
else if (path === "/api/models")
data = {
models: [
{
id: "image-test",
modelId: "gpt-image-2",
providerType: "openai",
name: "GPT Image",
type: "image",
enabled: true,
supportsImageGen: true,
imageGenConfig: { creditCostPerImage: 15 },
},
],
};
else if (
path === "/api/canvas/toolbar-test" &&
route.request().method() === "GET"
)
data = {
success: true,
data: {
id: "toolbar-test",
title: "工具栏测试",
canvasData: {
nodes: [
{
id: "toolbar-image",
type: "image",
position: { x: 220, y: 160 },
size: { width: 640, height: 420 },
data: {
status: "completed",
src: new URL("/toolbar-test.svg", page.url()).href,
title: "工具栏测试图",
originalWidth: 640,
originalHeight: 420,
},
},
],
edges: [],
viewport: { x: 0, y: 0, zoom: 1 },
},
},
};
else if (path.startsWith("/api/canvas/")) data = { success: true };
else if (path.includes("sessions")) data = { sessions: [] };
else if (path.includes("messages")) data = { messages: [] };
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify(data),
});
});
await context.route("**/toolbar-test.svg", (route) =>
route.fulfill({
contentType: "image/svg+xml",
body: '<svg xmlns="http://www.w3.org/2000/svg" width="640" height="420"><rect width="640" height="420" fill="#43b9bd"/></svg>',
})
);
const node = page.locator('[data-canvas-node-id="toolbar-image"]');
const bar = page.locator('[data-selected-image-toolbar="true"]');
const dialog = page.getByRole("dialog");
const keys = () =>
bar
.locator("button[data-tool-key]")
.evaluateAll((buttons) =>
buttons.map((button) => button.getAttribute("data-tool-key"))
);
const selectImage = async () => {
await node.waitFor();
await node.click({ position: { x: 80, y: 80 } });
await expect(bar).toBeVisible();
};
const customize = async () => {
await bar.getByRole("button", { name: "更多操作", exact: true }).click();
await page
.getByRole("menuitem", { name: "自定义工具栏", exact: true })
.click();
await expect(dialog).toBeVisible();
};
await page.goto("/canvas/toolbar-test", { waitUntil: "commit" });
await selectImage();
const initial = await keys();
await expect(bar).toHaveAttribute("data-show-tool-names", "false");
await bar.getByRole("button", { name: "高清", exact: true }).hover();
await expect(page.getByRole("tooltip")).toContainText("高清");
await page.mouse.move(1000, 100);
await customize();
await dialog.getByRole("button", { name: "固定裁剪", exact: true }).click();
await dialog.getByRole("button", { name: "取消", exact: true }).click();
expect(await keys()).toEqual(initial);
await customize();
await dialog
.getByRole("button", { name: "取消固定高清", exact: true })
.click();
await dialog.getByRole("button", { name: "固定裁剪", exact: true }).click();
await dialog.getByRole("switch", { name: "显示工具名称" }).click();
await dialog
.getByRole("button", { name: "裁剪,按左右方向键调整顺序", exact: true })
.focus();
await page.keyboard.press("ArrowLeft");
await dialog.getByRole("button", { name: "保存", exact: true }).click();
const changed = await keys();
expect(changed).toContain("crop");
expect(changed).not.toContain("enhance");
expect(changed.at(-3)).toBe("crop");
await expect(bar).toHaveAttribute("data-show-tool-names", "true");
await bar.getByRole("button", { name: "更多操作", exact: true }).click();
await expect(
page.getByRole("menuitem", { name: "高清", exact: true })
).toBeVisible();
await page.keyboard.press("Escape");
await page.reload({ waitUntil: "commit" });
await selectImage();
expect(await keys()).toEqual(changed);
await bar.getByRole("button", { name: "裁剪", exact: true }).click();
await expect(
page.getByRole("heading", { name: "裁剪", exact: true })
).toBeVisible();
await page.getByRole("button", { name: "取消", exact: true }).click();
await expect(bar).toBeVisible();
await bar.getByRole("button", { name: "添加对话", exact: true }).click();
await expect(page.getByText("已添加到对话框", { exact: true })).toBeVisible();
await customize();
await dialog
.getByRole("button", { name: "恢复默认工具栏", exact: true })
.click();
await dialog.getByRole("button", { name: "保存", exact: true }).click();
expect(await keys()).toEqual(initial);
await page.evaluate((key) => {
localStorage.setItem(key, "invalid-json{");
window.dispatchEvent(
new StorageEvent("storage", { key, newValue: "invalid-json{" })
);
}, storageKey);
expect(await keys()).toEqual(initial);
await customize();
const firstHandle = dialog.getByRole("button", {
name: "识物,按左右方向键调整顺序",
exact: true,
});
const secondHandle = dialog.getByRole("button", {
name: "高清,按左右方向键调整顺序",
exact: true,
});
await firstHandle.dragTo(secondHandle);
await dialog.getByRole("button", { name: "保存", exact: true }).click();
const draggedOrder = await keys();
expect(draggedOrder.slice(1, 3)).toEqual(["enhance", "pickObject"]);
await customize();
await page.setViewportSize({ width: 390, height: 700 });
await expect
.poll(async () => (await dialog.boundingBox())?.width)
.toBeLessThanOrEqual(358);
await expect
.poll(async () => (await dialog.boundingBox())?.height)
.toBeLessThanOrEqual(652);
await expect(
dialog.getByRole("button", { name: "保存", exact: true })
).toBeInViewport();
await dialog.getByRole("button", { name: "取消", exact: true }).click();
await page.setViewportSize({ width: 1512, height: 793 });
settings.canvasToolbarConfig = {
selectedImageInlineActionCount: 3,
items: {
imageEnhance: {
label: "自定义高清",
iconUrl: "/icons/lovart/model-gpt.svg",
enabled: true,
},
imageRemoveBackground: { enabled: false },
},
};
await page.reload({ waitUntil: "commit" });
await selectImage();
const customIcon = bar
.getByRole("button", { name: "自定义高清", exact: true })
.locator('span[aria-hidden="true"]');
expect(
await customIcon.evaluate(
(element) => (element as HTMLElement).style.maskImage
)
).toContain("model-gpt.svg");
await customize();
await expect(dialog.getByRole("button", { name: /固定抠图/ })).toHaveCount(0);
expect(errors).toEqual([]);
});