mirror of
https://github.com/YuzuZensai/Pien-Studio.git
synced 2026-09-02 14:18:35 +00:00
✨ feat: layer effects, bucket fill, simple brush
This commit is contained in:
@@ -1,8 +1,9 @@
|
||||
import React from "react";
|
||||
import type { FaceBlurMethod, Layer } from "@pien-studio/types";
|
||||
import type { Layer, LayerEffect } from "@pien-studio/types";
|
||||
import type { FaceDetectionOverlay } from "../../hooks/use-face-detection";
|
||||
import { CanvasRenderer } from "../canvas-renderer";
|
||||
import { CanvasContextMenu } from "./canvas-context-menu";
|
||||
import type { BrushOptions, BrushStroke } from "../../lib/brush-painter";
|
||||
|
||||
type EditorCanvasStageProps = {
|
||||
isDark: boolean;
|
||||
@@ -10,15 +11,13 @@ type EditorCanvasStageProps = {
|
||||
canvasWidth: number;
|
||||
canvasHeight: number;
|
||||
selectedLayerId: string | null;
|
||||
tool: "pointer" | "hand" | "face";
|
||||
tool: string;
|
||||
onFillLayer?: (layerId: string, x: number, y: number) => void;
|
||||
brushOptions?: BrushOptions;
|
||||
onBrushCommit?: (layerId: string, stroke: BrushStroke) => void;
|
||||
faceDetections: FaceDetectionOverlay[];
|
||||
faceOverlayLayerId: string | null;
|
||||
faceBlurPreview: {
|
||||
layerId: string;
|
||||
method: FaceBlurMethod;
|
||||
amount: number;
|
||||
regions: { x: number; y: number; width: number; height: number }[];
|
||||
} | null;
|
||||
faceBlurPreview: { layerId: string; effects: LayerEffect[] } | null;
|
||||
contextMenu: { x: number; y: number } | null;
|
||||
labels: { copy: string; cut: string; paste: string };
|
||||
onSelectLayer: (id: string | null) => void;
|
||||
@@ -64,6 +63,9 @@ export function EditorCanvasStage(props: EditorCanvasStageProps) {
|
||||
onCopy,
|
||||
onCut,
|
||||
onPaste,
|
||||
onFillLayer,
|
||||
brushOptions,
|
||||
onBrushCommit,
|
||||
} = props;
|
||||
|
||||
return (
|
||||
@@ -93,6 +95,9 @@ export function EditorCanvasStage(props: EditorCanvasStageProps) {
|
||||
onInteractionStart={onInteractionStart}
|
||||
onInteractionEnd={onInteractionEnd}
|
||||
onContextMenu={onContextMenu}
|
||||
onFillLayer={onFillLayer}
|
||||
brushOptions={brushOptions}
|
||||
onBrushCommit={onBrushCommit}
|
||||
isDark={isDark}
|
||||
tool={tool}
|
||||
faceDetections={faceDetections}
|
||||
|
||||
@@ -17,7 +17,7 @@ describe("EditorMobileSection", () => {
|
||||
layers: [],
|
||||
selectedLayerId: null,
|
||||
tool: "face",
|
||||
faceDetections: [{ x: 1, y: 1, width: 10, height: 10, label: "f" }],
|
||||
faceDetections: [{ x: 1, y: 1, width: 10, height: 10, label: "f", sourceWidth: 100, sourceHeight: 100 }],
|
||||
faceOverlayLayerId: null,
|
||||
faceStatus: "idle",
|
||||
faceBlurPreview: null,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from "react";
|
||||
import { CanvasRenderer } from "../canvas-renderer";
|
||||
import type { FaceBlurMethod, Layer } from "@pien-studio/types";
|
||||
import type { Layer, LayerEffect } from "@pien-studio/types";
|
||||
import type { FaceDetectionOverlay } from "../../hooks/use-face-detection";
|
||||
|
||||
type EditorMobileSectionProps = {
|
||||
@@ -9,16 +9,11 @@ type EditorMobileSectionProps = {
|
||||
canvasHeight: number;
|
||||
layers: Layer[];
|
||||
selectedLayerId: string | null;
|
||||
tool: "pointer" | "hand" | "face";
|
||||
tool: string;
|
||||
faceDetections: FaceDetectionOverlay[];
|
||||
faceOverlayLayerId: string | null;
|
||||
faceStatus: "idle" | "detecting" | "unsupported";
|
||||
faceBlurPreview: {
|
||||
layerId: string;
|
||||
method: FaceBlurMethod;
|
||||
amount: number;
|
||||
regions: { x: number; y: number; width: number; height: number }[];
|
||||
} | null;
|
||||
faceBlurPreview: { layerId: string; effects: LayerEffect[] } | null;
|
||||
labels: {
|
||||
resize: string;
|
||||
faceMlFailedShort: string;
|
||||
|
||||
@@ -7,7 +7,21 @@ import type { FaceDetectionOverlay, FacePreview } from "../../hooks/use-face-det
|
||||
|
||||
type EditorSidebarProps = {
|
||||
isDark: boolean;
|
||||
tool: "pointer" | "hand" | "face";
|
||||
tool: string;
|
||||
fillColor?: string;
|
||||
fillTolerance?: number;
|
||||
onSetFillColor?: (color: string) => void;
|
||||
onSetFillTolerance?: (tolerance: number) => void;
|
||||
onCreateFillLayer?: () => void;
|
||||
brushColor?: string;
|
||||
brushSize?: number;
|
||||
brushOpacity?: number;
|
||||
brushHardness?: number;
|
||||
onSetBrushColor?: (color: string) => void;
|
||||
onSetBrushSize?: (size: number) => void;
|
||||
onSetBrushOpacity?: (opacity: number) => void;
|
||||
onSetBrushHardness?: (hardness: number) => void;
|
||||
onAddLayer?: () => void;
|
||||
layers: Layer[];
|
||||
selectedLayerId: string | null;
|
||||
selectedLayer: Layer | null;
|
||||
@@ -22,6 +36,8 @@ type EditorSidebarProps = {
|
||||
censorColor: string;
|
||||
selectedFaceIndices: number[];
|
||||
onSelectLayer: (layerId: string | null) => void;
|
||||
onSetLayerVisible: (layerId: string, visible: boolean) => void;
|
||||
onSetEffectEnabled: (layerId: string, kind: string, enabled: boolean) => void; // string intentional: UI doesn't need the narrowed union
|
||||
onMoveLayerOrder: (direction: "up" | "down") => void;
|
||||
onRemoveSelectedLayer: () => void;
|
||||
onUndo: () => void;
|
||||
@@ -39,6 +55,20 @@ type EditorSidebarProps = {
|
||||
export function EditorSidebar({
|
||||
isDark,
|
||||
tool,
|
||||
fillColor,
|
||||
fillTolerance,
|
||||
onSetFillColor,
|
||||
onSetFillTolerance,
|
||||
onCreateFillLayer,
|
||||
brushColor,
|
||||
brushSize,
|
||||
brushOpacity,
|
||||
brushHardness,
|
||||
onSetBrushColor,
|
||||
onSetBrushSize,
|
||||
onSetBrushOpacity,
|
||||
onSetBrushHardness,
|
||||
onAddLayer,
|
||||
layers,
|
||||
selectedLayerId,
|
||||
selectedLayer,
|
||||
@@ -53,6 +83,8 @@ export function EditorSidebar({
|
||||
censorColor,
|
||||
selectedFaceIndices,
|
||||
onSelectLayer,
|
||||
onSetLayerVisible,
|
||||
onSetEffectEnabled,
|
||||
onMoveLayerOrder,
|
||||
onRemoveSelectedLayer,
|
||||
onUndo,
|
||||
@@ -74,10 +106,86 @@ export function EditorSidebar({
|
||||
selectedLayerId={selectedLayerId}
|
||||
isDark={isDark}
|
||||
onSelectLayer={(layerId) => onSelectLayer(layerId)}
|
||||
onSetLayerVisible={onSetLayerVisible}
|
||||
onSetEffectEnabled={onSetEffectEnabled}
|
||||
onMoveLayerOrder={onMoveLayerOrder}
|
||||
onRemoveSelectedLayer={onRemoveSelectedLayer}
|
||||
onAddLayer={onAddLayer}
|
||||
/>
|
||||
|
||||
{tool === "fill" && onSetFillColor && onSetFillTolerance ? (
|
||||
<div className={`rounded-lg border p-3 ${isDark ? "border-white/10 bg-[#2d3036]" : "border-black/10 bg-white"}`}>
|
||||
<p className={`mb-2 text-xs font-semibold uppercase tracking-wider ${isDark ? "text-[#8b9ab1]" : "text-[#6b7280]"}`}>
|
||||
Fill
|
||||
</p>
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<label className={`text-xs ${isDark ? "text-[#d7dae0]" : "text-[#1f2430]"}`}>Color</label>
|
||||
<input
|
||||
type="color"
|
||||
value={fillColor ?? "#ff0000"}
|
||||
onChange={(e) => onSetFillColor(e.target.value)}
|
||||
className="h-7 w-10 cursor-pointer rounded border border-black/10 p-0.5"
|
||||
/>
|
||||
<span className={`text-xs font-mono ${isDark ? "text-[#d7dae0]" : "text-[#1f2430]"}`}>{fillColor ?? "#ff0000"}</span>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="flex items-center justify-between">
|
||||
<label className={`text-xs ${isDark ? "text-[#d7dae0]" : "text-[#1f2430]"}`}>Tolerance</label>
|
||||
<span className={`text-xs font-mono ${isDark ? "text-[#8b9ab1]" : "text-[#6b7280]"}`}>{fillTolerance ?? 32}</span>
|
||||
</div>
|
||||
<input
|
||||
type="range"
|
||||
min={0}
|
||||
max={255}
|
||||
value={fillTolerance ?? 32}
|
||||
onChange={(e) => onSetFillTolerance(Number(e.target.value))}
|
||||
className="w-full accent-[var(--color-accent-strong)]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{tool === "brush" && onSetBrushColor && onSetBrushSize && onSetBrushOpacity && onSetBrushHardness ? (
|
||||
<div className={`rounded-lg border p-3 ${isDark ? "border-white/10 bg-[#2d3036]" : "border-black/10 bg-white"}`}>
|
||||
<p className={`mb-2 text-xs font-semibold uppercase tracking-wider ${isDark ? "text-[#8b9ab1]" : "text-[#6b7280]"}`}>
|
||||
Brush
|
||||
</p>
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<label className={`text-xs ${isDark ? "text-[#d7dae0]" : "text-[#1f2430]"}`}>Color</label>
|
||||
<input
|
||||
type="color"
|
||||
value={brushColor ?? "#000000"}
|
||||
onChange={(e) => onSetBrushColor(e.target.value)}
|
||||
className="h-7 w-10 cursor-pointer rounded border border-black/10 p-0.5"
|
||||
/>
|
||||
<span className={`text-xs font-mono ${isDark ? "text-[#d7dae0]" : "text-[#1f2430]"}`}>{brushColor ?? "#000000"}</span>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="flex items-center justify-between">
|
||||
<label className={`text-xs ${isDark ? "text-[#d7dae0]" : "text-[#1f2430]"}`}>Size</label>
|
||||
<span className={`text-xs font-mono ${isDark ? "text-[#8b9ab1]" : "text-[#6b7280]"}`}>{brushSize ?? 20}px</span>
|
||||
</div>
|
||||
<input type="range" min={1} max={200} value={brushSize ?? 20} onChange={(e) => onSetBrushSize(Number(e.target.value))} className="w-full accent-[var(--color-accent-strong)]" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="flex items-center justify-between">
|
||||
<label className={`text-xs ${isDark ? "text-[#d7dae0]" : "text-[#1f2430]"}`}>Opacity</label>
|
||||
<span className={`text-xs font-mono ${isDark ? "text-[#8b9ab1]" : "text-[#6b7280]"}`}>{Math.round((brushOpacity ?? 1) * 100)}%</span>
|
||||
</div>
|
||||
<input type="range" min={0} max={100} value={Math.round((brushOpacity ?? 1) * 100)} onChange={(e) => onSetBrushOpacity(Number(e.target.value) / 100)} className="w-full accent-[var(--color-accent-strong)]" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="flex items-center justify-between">
|
||||
<label className={`text-xs ${isDark ? "text-[#d7dae0]" : "text-[#1f2430]"}`}>Hardness</label>
|
||||
<span className={`text-xs font-mono ${isDark ? "text-[#8b9ab1]" : "text-[#6b7280]"}`}>{Math.round((brushHardness ?? 0.8) * 100)}%</span>
|
||||
</div>
|
||||
<input type="range" min={0} max={100} value={Math.round((brushHardness ?? 0.8) * 100)} onChange={(e) => onSetBrushHardness(Number(e.target.value) / 100)} className="w-full accent-[var(--color-accent-strong)]" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{tool === "face" ? (
|
||||
<FacePanel
|
||||
isDark={isDark}
|
||||
@@ -89,7 +197,7 @@ export function EditorSidebar({
|
||||
blurAmount={blurAmount}
|
||||
censorColor={censorColor}
|
||||
selectedFaceIndices={selectedFaceIndices}
|
||||
hasActiveBlur={Boolean(selectedLayer && selectedLayer.type === "image" && selectedLayer.faceBlur)}
|
||||
hasActiveBlur={Boolean(selectedLayer && selectedLayer.effects.some((e) => e.kind === "face-blur"))}
|
||||
onSetBlurMethod={onSetBlurMethod}
|
||||
onSetBlurAmount={onSetBlurAmount}
|
||||
onSetCensorColor={onSetCensorColor}
|
||||
|
||||
@@ -57,7 +57,7 @@ export function FacePanel({
|
||||
? t("editor.faceMlFailed")
|
||||
: faceStatus === "detecting"
|
||||
? t("editor.detectingFaces")
|
||||
: selectedLayer?.type !== "image"
|
||||
: selectedLayer?.type !== "raster"
|
||||
? t("editor.selectImageLayer")
|
||||
: faceDetections.length === 0
|
||||
? t("editor.noFacesFound")
|
||||
|
||||
@@ -2,69 +2,146 @@
|
||||
|
||||
import type { Layer } from "@pien-studio/types";
|
||||
import Image from "next/image";
|
||||
import { Eye, EyeOff } from "lucide-react";
|
||||
import { useTranslations } from "../../hooks/use-translations";
|
||||
import { panelClass, panelCounterClass, panelInsetClass, panelTitleClass } from "../../lib/theme";
|
||||
|
||||
const EFFECT_LABELS: Record<string, string> = {
|
||||
"face-blur": "Face Blur",
|
||||
};
|
||||
|
||||
type Props = {
|
||||
layers: Layer[];
|
||||
selectedLayerId: string | null;
|
||||
isDark: boolean;
|
||||
onSelectLayer: (layerId: string) => void;
|
||||
onSetLayerVisible: (layerId: string, visible: boolean) => void;
|
||||
onSetEffectEnabled: (layerId: string, kind: string, enabled: boolean) => void;
|
||||
onMoveLayerOrder: (direction: "up" | "down") => void;
|
||||
onRemoveSelectedLayer: () => void;
|
||||
onAddLayer?: () => void;
|
||||
};
|
||||
|
||||
export function LayersPanel({ layers, selectedLayerId, isDark, onSelectLayer, onMoveLayerOrder, onRemoveSelectedLayer }: Props) {
|
||||
export function LayersPanel({ layers, selectedLayerId, isDark, onSelectLayer, onSetLayerVisible, onSetEffectEnabled, onMoveLayerOrder, onRemoveSelectedLayer, onAddLayer }: Props) {
|
||||
const { t } = useTranslations();
|
||||
|
||||
return (
|
||||
<div className={panelClass(isDark)}>
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className={`text-xs font-semibold uppercase tracking-wide ${panelTitleClass(isDark)}`}>{t("editor.layers")}</h2>
|
||||
<span className={`rounded-full px-2 py-0.5 text-[10px] font-semibold ${panelCounterClass(isDark)}`}>
|
||||
{layers.length}
|
||||
</span>
|
||||
<div className="flex items-center gap-1.5">
|
||||
{onAddLayer ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onAddLayer}
|
||||
title={t("editor.newLayer")}
|
||||
className={`rounded border px-1.5 py-0.5 text-[10px] font-semibold ${isDark ? "border-white/20 text-[#d7dae0] hover:bg-white/10" : "border-black/20 text-[#1f2430] hover:bg-black/5"}`}
|
||||
>
|
||||
+ New
|
||||
</button>
|
||||
) : null}
|
||||
<span className={`rounded-full px-2 py-0.5 text-[10px] font-semibold ${panelCounterClass(isDark)}`}>
|
||||
{layers.length}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className={`mt-3 max-h-[320px] space-y-1 overflow-auto rounded-md border p-1 ${panelInsetClass(isDark)}`}>
|
||||
<div className={`mt-3 max-h-[420px] space-y-0.5 overflow-auto rounded-md border p-1 ${panelInsetClass(isDark)}`}>
|
||||
{layers.slice().reverse().map((layer, idx) => {
|
||||
const isSelected = layer.id === selectedLayerId;
|
||||
const isImageLayer = layer.type === "image" || layer.type === "sticker";
|
||||
const isRaster = layer.type === "raster" || layer.type === "sticker";
|
||||
const hasEffects = layer.effects.length > 0;
|
||||
const isHidden = layer.visible === false;
|
||||
return (
|
||||
<button
|
||||
key={layer.id}
|
||||
type="button"
|
||||
onClick={() => onSelectLayer(layer.id)}
|
||||
className={`group flex w-full items-center justify-between gap-2 rounded px-2 py-1.5 text-left text-xs transition ${
|
||||
<div key={layer.id}>
|
||||
<div className={`group flex w-full items-center gap-2 rounded px-2 py-2 text-xs transition ${
|
||||
isSelected
|
||||
? "bg-[var(--color-accent-strong)] text-white"
|
||||
: isDark
|
||||
? "text-[#d7dae0] hover:bg-white/10"
|
||||
: "text-[#1f2430] hover:bg-black/5"
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className={`w-6 text-[10px] font-semibold ${isSelected ? "text-white/90" : isDark ? "text-[#9aa1ad]" : "text-[#6b7280]"}`}>{idx + 1}</span>
|
||||
<div className={`h-9 w-9 shrink-0 overflow-hidden rounded border ${isSelected ? "border-white/60 bg-white/10" : isDark ? "border-white/15 bg-[#1f2126]" : "border-black/10 bg-[#eef1f6]"}`}>
|
||||
{isImageLayer && layer.sourceUri ? (
|
||||
<Image src={layer.sourceUri} alt={layer.name ?? layer.type} width={36} height={36} unoptimized className="h-full w-full object-cover" draggable={false} />
|
||||
) : (
|
||||
<div className={`flex h-full w-full items-center justify-center text-[9px] font-semibold uppercase tracking-wide ${isSelected ? "text-white/90" : isDark ? "text-[#b7bdc8]" : "text-[#596274]"}`}>
|
||||
{layer.type}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-semibold">{layer.name ?? layer.type}</p>
|
||||
<p className={`${isSelected ? "text-white/80" : isDark ? "text-[#9aa1ad]" : "text-[#7b8392]"}`}>{layer.type}</p>
|
||||
</div>
|
||||
} ${isHidden ? "opacity-40" : ""}`}>
|
||||
{/* Thumbnail */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onSelectLayer(layer.id)}
|
||||
className="shrink-0"
|
||||
>
|
||||
<div className={`h-10 w-10 overflow-hidden rounded border ${isSelected ? "border-white/40 bg-white/10" : isDark ? "border-white/15 bg-[#1a1c20]" : "border-black/10 bg-[#eef1f6]"}`}>
|
||||
{isRaster && layer.sourceUri ? (
|
||||
<Image src={layer.sourceUri} alt={layer.name ?? layer.type} width={40} height={40} unoptimized className="h-full w-full object-cover" draggable={false} />
|
||||
) : (
|
||||
<div className={`flex h-full w-full items-center justify-center text-[8px] font-bold uppercase tracking-wide ${isSelected ? "text-white/80" : isDark ? "text-[#b7bdc8]" : "text-[#596274]"}`}>
|
||||
{layer.type}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</button>
|
||||
|
||||
{/* Info */}
|
||||
<button type="button" onClick={() => onSelectLayer(layer.id)} className="min-w-0 flex-1 text-left">
|
||||
<p className="truncate font-semibold leading-tight">{layer.name ?? layer.type}</p>
|
||||
<p className={`text-[10px] leading-tight mt-0.5 ${isSelected ? "text-white/70" : isDark ? "text-[#9aa1ad]" : "text-[#7b8392]"}`}>
|
||||
{layer.type}
|
||||
{layer.opacity < 1 ? ` · ${Math.round(layer.opacity * 100)}%` : ""}
|
||||
</p>
|
||||
</button>
|
||||
|
||||
{/* Visibility toggle */}
|
||||
<button
|
||||
type="button"
|
||||
title={isHidden ? "Show layer" : "Hide layer"}
|
||||
onClick={(e) => { e.stopPropagation(); onSetLayerVisible(layer.id, !isHidden); }}
|
||||
className={`shrink-0 rounded p-0.5 opacity-0 group-hover:opacity-100 transition-opacity ${isHidden ? "!opacity-100" : ""} ${isSelected ? "hover:bg-white/20" : isDark ? "hover:bg-white/10" : "hover:bg-black/10"}`}
|
||||
>
|
||||
{isHidden
|
||||
? <EyeOff className="h-3.5 w-3.5" />
|
||||
: <Eye className="h-3.5 w-3.5" />}
|
||||
</button>
|
||||
|
||||
{/* Layer index */}
|
||||
<span className={`shrink-0 text-[10px] font-semibold ${isSelected ? "text-white/60" : isDark ? "text-[#9aa1ad]" : "text-[#6b7280]"}`}>
|
||||
{layers.length - idx}
|
||||
</span>
|
||||
</div>
|
||||
<span className={`h-1.5 w-1.5 rounded-full ${isSelected ? "bg-white" : isDark ? "bg-[#3d424c]" : "bg-[#d4d8e0]"}`} />
|
||||
</button>
|
||||
|
||||
{/* Effects chips */}
|
||||
{hasEffects ? (
|
||||
<div className="ml-12 mb-0.5 flex flex-wrap gap-1 px-1">
|
||||
{layer.effects.map((effect) => {
|
||||
const isDisabled = effect.enabled === false;
|
||||
return (
|
||||
<button
|
||||
key={effect.kind}
|
||||
type="button"
|
||||
title={isDisabled ? "Enable effect" : "Disable effect"}
|
||||
onClick={() => onSetEffectEnabled(layer.id, effect.kind, isDisabled)}
|
||||
className={`flex items-center gap-1 rounded px-1.5 py-0.5 text-[9px] font-semibold transition ${
|
||||
isDisabled
|
||||
? isDark ? "bg-white/10 text-[#6b7280] line-through" : "bg-black/5 text-[#9ca3af] line-through"
|
||||
: isSelected
|
||||
? "bg-white/20 text-white"
|
||||
: isDark
|
||||
? "bg-[var(--color-accent-strong)]/20 text-[var(--color-accent-strong)]"
|
||||
: "bg-[var(--color-accent-strong)]/15 text-[var(--color-accent-strong)]"
|
||||
}`}
|
||||
>
|
||||
{isDisabled ? <EyeOff className="h-2.5 w-2.5" /> : <Eye className="h-2.5 w-2.5" />}
|
||||
{EFFECT_LABELS[effect.kind] ?? effect.kind}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{layers.length === 0 ? <div className={`px-2 py-6 text-center text-xs ${isDark ? "text-[#aeb3bc]" : "text-[#5f6672]"}`}>{t("editor.noLayersYet")}</div> : null}
|
||||
{layers.length === 0 ? (
|
||||
<div className={`px-2 py-8 text-center text-xs ${isDark ? "text-[#aeb3bc]" : "text-[#5f6672]"}`}>
|
||||
{t("editor.noLayersYet")}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="mt-3 flex gap-2">
|
||||
<div className="mt-2 flex gap-1.5">
|
||||
<button type="button" onClick={() => onMoveLayerOrder("up")} className={`flex-1 rounded border px-2 py-1 text-xs ${isDark ? "border-white/20 text-[#d7dae0] hover:bg-white/10" : "border-black/20 text-[#1f2430] hover:bg-black/5"}`}>{t("editor.up")}</button>
|
||||
<button type="button" onClick={() => onMoveLayerOrder("down")} className={`flex-1 rounded border px-2 py-1 text-xs ${isDark ? "border-white/20 text-[#d7dae0] hover:bg-white/10" : "border-black/20 text-[#1f2430] hover:bg-black/5"}`}>{t("editor.down")}</button>
|
||||
<button type="button" onClick={onRemoveSelectedLayer} className={`flex-1 rounded border px-2 py-1 text-xs ${isDark ? "border-red-400/30 bg-red-400/10 text-red-200 hover:bg-red-400/20" : "border-red-500/30 bg-red-500/10 text-red-600 hover:bg-red-500/15"}`}>{t("editor.delete")}</button>
|
||||
|
||||
Reference in New Issue
Block a user