Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
114 changes: 111 additions & 3 deletions src/components/image-editor/LayersPanel.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { type DragEvent } from 'react'
import { type DragEvent, useState } from 'react'
import { useTranslation } from 'react-i18next'
import {
ChevronDown,
Expand All @@ -20,7 +20,8 @@ import {
removeAtPath,
} from '@/lib/image-editor/layer-tree'
import { hasEffects } from '@/lib/image-editor/layer-effects'
import type { EditorState, GroupLayer, Layer } from '@/lib/image-editor/types'
import type { EditorState, GroupLayer, Layer, LayerColorTag } from '@/lib/image-editor/types'
import { LAYER_TAG_COLORS, LAYER_TAGS } from '@/lib/image-editor/layer-color-tags'

type Props = {
state: EditorState
Expand All @@ -37,6 +38,14 @@ type Props = {
/** Add a mask to the given layer. For annotation / SO / group: insert a
* new raster MaskLayer above. For adjustment / filter: add maskDataUrl. */
onAddMask?: (id: string) => void
/** Which row is in inline-rename mode (double-click / context menu Rename). */
renamingId?: string | null
/** Begin inline rename for a row (double-click). */
onStartRename?: (id: string) => void
/** Commit a rename (Enter / blur) and exit edit mode. */
onCommitRename?: (id: string, name: string) => void
/** Set / clear a layer's organisational color tag. */
onSetColorTag?: (id: string, tag: LayerColorTag | undefined) => void
}

type DropMode = 'into' | 'sibling' | 'top'
Expand Down Expand Up @@ -66,6 +75,10 @@ export function LayersPanel({
onOpenStyle,
onLayerContextMenu,
onAddMask,
renamingId,
onStartRename,
onCommitRename,
onSetColorTag,
}: Props) {
const { t } = useTranslation()

Expand Down Expand Up @@ -156,6 +169,10 @@ export function LayersPanel({
onOpenStyle={onOpenStyle}
onLayerContextMenu={onLayerContextMenu}
onAddMask={onAddMask}
renamingId={renamingId}
onStartRename={onStartRename}
onCommitRename={onCommitRename}
onSetColorTag={onSetColorTag}
/>
))}

Expand Down Expand Up @@ -225,6 +242,10 @@ function LayerSubtree({
onOpenStyle,
onLayerContextMenu,
onAddMask,
renamingId,
onStartRename,
onCommitRename,
onSetColorTag,
}: {
layer: Layer
depth: number
Expand All @@ -238,6 +259,10 @@ function LayerSubtree({
onOpenStyle: (id: string) => void
onLayerContextMenu?: (id: string, x: number, y: number) => void
onAddMask?: (id: string) => void
renamingId?: string | null
onStartRename?: (id: string) => void
onCommitRename?: (id: string, name: string) => void
onSetColorTag?: (id: string, tag: LayerColorTag | undefined) => void
}) {
const group = isGroup(layer) ? layer : null
return (
Expand All @@ -257,6 +282,10 @@ function LayerSubtree({
onOpenStyle={() => onOpenStyle(layer.id)}
onContextMenu={(x, y) => onLayerContextMenu?.(layer.id, x, y)}
onAddMask={onAddMask}
renaming={renamingId === layer.id}
onStartRename={() => onStartRename?.(layer.id)}
onCommitRename={(name) => onCommitRename?.(layer.id, name)}
onSetColorTag={(tag) => onSetColorTag?.(layer.id, tag)}
/>
{group && group.expanded &&
[...group.children].reverse().map((c) => (
Expand All @@ -274,6 +303,10 @@ function LayerSubtree({
onOpenStyle={onOpenStyle}
onLayerContextMenu={onLayerContextMenu}
onAddMask={onAddMask}
renamingId={renamingId}
onStartRename={onStartRename}
onCommitRename={onCommitRename}
onSetColorTag={onSetColorTag}
/>
))}
</>
Expand Down Expand Up @@ -304,6 +337,10 @@ function LayerRow({
onOpenStyle,
onContextMenu,
onAddMask,
renaming,
onStartRename,
onCommitRename,
onSetColorTag,
}: {
layer: Layer
depth: number
Expand All @@ -319,10 +356,15 @@ function LayerRow({
onOpenStyle: () => void
onContextMenu?: (x: number, y: number) => void
onAddMask?: (layerId: string) => void
renaming?: boolean
onStartRename?: () => void
onCommitRename?: (name: string) => void
onSetColorTag?: (tag: LayerColorTag | undefined) => void
}) {
const { t } = useTranslation()
const showFx = hasEffects(layer)
const labelKey = layerLabelKey(layer)
const [colorMenuOpen, setColorMenuOpen] = useState(false)
const labelArgs =
layer.kind === 'annotation' && layer.shape.kind === 'text'
? {
Expand Down Expand Up @@ -414,7 +456,73 @@ function LayerRow({
style={{ background: '#000' }}
/>
)}
<span className="flex-1 truncate">{t(labelKey, labelArgs)}</span>
{/* Color tag dot — click cycles open a tiny swatch row. */}
{onSetColorTag && (
<span className="relative flex items-center">
<button
onClick={(e) => {
e.stopPropagation()
setColorMenuOpen((v) => !v)
}}
className="h-2.5 w-2.5 rounded-full border border-input"
style={{ background: layer.colorTag ? LAYER_TAG_COLORS[layer.colorTag] : 'transparent' }}
title={t('pages.imageEditor.layers.colorTag')}
/>
{colorMenuOpen && (
<span
className="pf-tag-popover absolute left-0 top-4 z-20 flex gap-1 rounded border border-border bg-popover p-1 shadow-md"
onClick={(e) => e.stopPropagation()}
>
{LAYER_TAGS.map((tag) => (
<button
key={tag}
className="h-3.5 w-3.5 rounded-full border border-input"
style={{ background: LAYER_TAG_COLORS[tag] }}
title={tag}
onClick={() => {
onSetColorTag(tag)
setColorMenuOpen(false)
}}
/>
))}
<button
className="h-3.5 w-3.5 rounded-full border border-input text-[8px] leading-none text-muted-foreground"
title={t('pages.imageEditor.layers.colorTagNone')}
onClick={() => {
onSetColorTag(undefined)
setColorMenuOpen(false)
}}
>
</button>
</span>
)}
</span>
)}
{renaming ? (
<input
autoFocus
defaultValue={layer.name}
className="flex-1 min-w-0 rounded border border-input bg-background px-1 text-xs"
onClick={(e) => e.stopPropagation()}
onKeyDown={(e) => {
e.stopPropagation()
if (e.key === 'Enter') onCommitRename?.((e.target as HTMLInputElement).value)
else if (e.key === 'Escape') onCommitRename?.(layer.name)
}}
onBlur={(e) => onCommitRename?.(e.target.value)}
/>
) : (
<span
className="flex-1 truncate"
onDoubleClick={(e) => {
e.stopPropagation()
onStartRename?.()
}}
>
{layer.name && layer.name !== t(labelKey, labelArgs) ? layer.name : t(labelKey, labelArgs)}
</span>
)}
{showFx && (
<button
onClick={(e) => {
Expand Down
29 changes: 29 additions & 0 deletions src/components/image-editor/MenuBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,12 @@ type Props = {
mergeVisible?: () => void
stampVisible?: () => void
flatten?: () => void
/** Rasterize the selected layer to plain pixels. */
rasterizeLayer?: () => void
/** Copy / Paste / Clear the selected layer's fx stack. */
copyLayerStyle?: () => void
pasteLayerStyle?: () => void
clearLayerStyle?: () => void
/** Open Layer Style dialog. `kind` preselects an effect; undefined = "Blending Options" (no preselect). */
openLayerStyle?: (kind?: LayerEffectKind) => void
/** PS Type on Path — create a TextLayer that follows the selected path. */
Expand Down Expand Up @@ -881,6 +887,29 @@ export function MenuBar({ handlers }: Props) {
label: t('pages.imageEditor.menu.flatten'),
onClick: handlers.flatten,
},
{
id: 'rasterizeLayer',
label: t('pages.imageEditor.menu.rasterizeLayer'),
onClick: handlers.rasterizeLayer,
},
],
{ sep: true },
[
{
id: 'copyLayerStyle',
label: t('pages.imageEditor.menu.copyLayerStyle'),
onClick: handlers.copyLayerStyle,
},
{
id: 'pasteLayerStyle',
label: t('pages.imageEditor.menu.pasteLayerStyle'),
onClick: handlers.pasteLayerStyle,
},
{
id: 'clearLayerStyle',
label: t('pages.imageEditor.menu.clearLayerStyle'),
onClick: handlers.clearLayerStyle,
},
],
{ sep: true },
[
Expand Down
15 changes: 14 additions & 1 deletion src/components/image-editor/RightSidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { LayersPanel } from './LayersPanel'
import { PathsPanel } from './PathsPanel'
import { PropertiesPanel } from './PropertiesPanel'
import type { ImageCache } from '@/lib/image-editor/drawing'
import type { Action, Adjustments, BrushOptions, EditorState, Layer, LayerComp, Transforms } from '@/lib/image-editor/types'
import type { Action, Adjustments, BrushOptions, EditorState, Layer, LayerColorTag, LayerComp, Transforms } from '@/lib/image-editor/types'

const LAYERS_HEIGHT_KEY = 'pf-layers-h'
const LAYERS_HEIGHT_DEFAULT = 320
Expand All @@ -37,6 +37,11 @@ type Props = {
/** Inline +Mask button in the layer row — adds an adjustment mask or a
* new MaskLayer above, depending on the target layer's kind. */
onAddMaskToLayer?: (id: string) => void
/** Layer rename (inline) + color tag plumbing → LayersPanel. */
renamingLayerId?: string | null
onStartRenameLayer?: (id: string) => void
onCommitRenameLayer?: (id: string, name: string) => void
onSetLayerColorTag?: (id: string, tag: LayerColorTag | undefined) => void
/** Paths panel — convert active selection ↔ vector path layer. */
onMakeWorkPath?: () => void
onMakeSelectionFromPath?: () => void
Expand Down Expand Up @@ -96,6 +101,10 @@ export function RightSidebar({
onReplaceSmartObjectContents,
onLayerContextMenu,
onAddMaskToLayer,
renamingLayerId,
onStartRenameLayer,
onCommitRenameLayer,
onSetLayerColorTag,
onMakeWorkPath,
onMakeSelectionFromPath,
image,
Expand Down Expand Up @@ -187,6 +196,10 @@ export function RightSidebar({
onOpenStyle={onOpenStyle}
onLayerContextMenu={onLayerContextMenu}
onAddMask={onAddMaskToLayer}
renamingId={renamingLayerId}
onStartRename={onStartRenameLayer}
onCommitRename={onCommitRenameLayer}
onSetColorTag={onSetLayerColorTag}
/>
</div>
)}
Expand Down
23 changes: 22 additions & 1 deletion src/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -1242,6 +1242,11 @@
"ungroupLayers": "Ungroup Layer",
"mergeDown": "Merge Down",
"mergeVisible": "Merge Visible",
"rasterizeLayer": "Rasterize Layer",
"renameLayer": "Rename Layer",
"copyLayerStyle": "Copy Layer Style",
"pasteLayerStyle": "Paste Layer Style",
"clearLayerStyle": "Clear Layer Style",
"stampVisible": "Stamp Visible",
"flatten": "Flatten Image",
"convertToSmartObject": "Convert to Smart Object",
Expand Down Expand Up @@ -1774,7 +1779,23 @@
"dropOutside": "Drop here to move out of group",
"resizeHandle": "Drag to resize Layers panel",
"editStyle": "Edit Layer Style",
"addMask": "Add layer mask"
"addMask": "Add layer mask",
"colorTag": "Color label",
"colorTagNone": "No color"
},
"rasterize": {
"done": "Layer rasterized",
"failed": "Could not rasterize layer",
"noBbox": "Layer has no pixels to rasterize",
"unsupportedKind": "Adjustment, filter, and mask layers can't be rasterized directly.",
"alreadyRaster": "Layer is already a plain raster layer"
},
"layerStyleClip": {
"copied": "Layer style copied",
"pasted": "Layer style pasted",
"cleared": "Layer style cleared",
"empty": "This layer has no style to copy",
"nothingToPaste": "No layer style on the clipboard"
},
"layerStyle": {
"title": "Layer Style",
Expand Down
23 changes: 22 additions & 1 deletion src/i18n/zh-CN.json
Original file line number Diff line number Diff line change
Expand Up @@ -1242,6 +1242,11 @@
"ungroupLayers": "取消图层编组",
"mergeDown": "向下合并",
"mergeVisible": "合并可见图层",
"rasterizeLayer": "栅格化图层",
"renameLayer": "重命名图层",
"copyLayerStyle": "拷贝图层样式",
"pasteLayerStyle": "粘贴图层样式",
"clearLayerStyle": "清除图层样式",
"stampVisible": "盖印可见图层",
"flatten": "拼合图像",
"convertToSmartObject": "转换为智能对象",
Expand Down Expand Up @@ -1774,7 +1779,23 @@
"dropOutside": "拖到此处移出组",
"resizeHandle": "拖动调整图层面板高度",
"editStyle": "编辑图层样式",
"addMask": "添加图层蒙版"
"addMask": "添加图层蒙版",
"colorTag": "颜色标签",
"colorTagNone": "无颜色"
},
"rasterize": {
"done": "已栅格化图层",
"failed": "无法栅格化该图层",
"noBbox": "该图层没有可栅格化的像素",
"unsupportedKind": "调整、滤镜和蒙版图层不能直接栅格化。",
"alreadyRaster": "该图层已经是普通栅格图层"
},
"layerStyleClip": {
"copied": "已拷贝图层样式",
"pasted": "已粘贴图层样式",
"cleared": "已清除图层样式",
"empty": "该图层没有可拷贝的样式",
"nothingToPaste": "剪贴板上没有图层样式"
},
"layerStyle": {
"title": "图层样式",
Expand Down
27 changes: 27 additions & 0 deletions src/lib/image-editor/layer-color-tags.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import type { LayerColorTag } from './types'

/**
* PS-style layer color labels → CSS color, plus the ordered list for the
* picker. Kept in lib (not the LayersPanel component) so it can be imported by
* both the panel and any future consumer without tripping react-refresh's
* "components-only export" rule.
*/
export const LAYER_TAG_COLORS: Record<LayerColorTag, string> = {
red: '#d9433c',
orange: '#e08a3c',
yellow: '#d7b13a',
green: '#4f9d52',
blue: '#3a8cff',
violet: '#8a63d2',
gray: '#8a8f98',
}

export const LAYER_TAGS: LayerColorTag[] = [
'red',
'orange',
'yellow',
'green',
'blue',
'violet',
'gray',
]
Loading
Loading