feat: add layer and mask thumbnails

This commit is contained in:
syntaxbullet
2026-07-11 12:20:26 +02:00
parent 9fb3a19912
commit b928fb599b
4 changed files with 213 additions and 3 deletions

View File

@@ -12,6 +12,8 @@ import { resolveLayerDrop } from "@input/index";
import type { DocumentActions } from "@app/document-actions";
import { addArtboard, addEmptyLayer, addGroupLayer, addLayerMask, deleteSelection, groupLayers, moveLayer } from "@operations/document/layerActions";
import { MaskOperationButtons, MaskStatus } from "./layers/MaskControls";
import { LayerThumbnail } from "./layers/LayerThumbnail";
import { createLayerThumbnailIndex, type LayerThumbnailModel } from "./layers/thumbnailModel";
export type LayersSheetProps = {
document: ImageDocument;
@@ -72,6 +74,7 @@ function LayersSheetBody({
const canGroup = Boolean(selection.artboardId && selection.layerIds.length > 0);
const canUngroup = selectedLayer?.layer.type === "group";
const maskLayerIds = documentIndex.maskLayerIds;
const thumbnailByLayerId = useMemo(() => createLayerThumbnailIndex(document, documentIndex.assetById, maskLayerIds), [document, documentIndex, maskLayerIds]);
return (
<>
@@ -182,6 +185,7 @@ function LayersSheetBody({
setEditingTitle={setEditingTitle}
maskLayerIds={maskLayerIds}
maskEdit={maskEdit}
thumbnailByLayerId={thumbnailByLayerId}
dispatch={dispatch}
/>
))
@@ -207,6 +211,7 @@ function LayerRow({
setEditingTitle,
maskLayerIds,
maskEdit,
thumbnailByLayerId,
dispatch,
}: {
document: ImageDocument;
@@ -220,6 +225,7 @@ function LayerRow({
setEditingTitle: (editingTitle: EditingTitle | undefined) => void;
maskLayerIds: ReadonlySet<string>;
maskEdit?: MaskEditState;
thumbnailByLayerId: ReadonlyMap<string, LayerThumbnailModel>;
dispatch: AppStore["dispatch"];
}) {
if (maskLayerIds.has(layer.id)) return null;
@@ -231,6 +237,8 @@ function LayerRow({
const maskAsset = maskLayer && maskLayer.type !== "group" ? documentIndex.assetById.get(maskLayer.assetId) : undefined;
const canAddMask = Boolean(layerInfo && layer.type !== "group" && !layerMask);
const editingMask = Boolean(maskEdit && layerMask && maskEdit.targetLayerId === layer.id && maskEdit.maskLayerId === layerMask.maskLayerId);
const thumbnail = thumbnailByLayerId.get(layer.id) ?? { kind: "empty" };
const maskThumbnail = maskLayer ? thumbnailByLayerId.get(maskLayer.id) : undefined;
const rowPadding = 12 + depth * 16;
return (
@@ -260,6 +268,14 @@ function LayerRow({
<button type="button" className={editingMask || selected ? "text-black/55" : "text-white/45 transition hover:text-white"} aria-label={layer.visible ? "Hide layer" : "Show layer"} onClick={() => dispatch(commandIds.documentSetLayerVisible, { layerId: layer.id, visible: !layer.visible })}>
{layer.visible ? <Eye size={24} weight="regular" /> : <EyeSlash size={24} weight="regular" />}
</button>
<span className={`relative flex shrink-0 items-center ${layer.visible ? "" : "opacity-35 grayscale"}`}>
<LayerThumbnail model={thumbnail} label={`${layer.name} thumbnail`} />
{maskThumbnail ? (
<span className={`-ml-2 rounded-md ${editingMask ? "ring-2 ring-sky-500 ring-offset-2 ring-offset-sky-300" : "ring-1 ring-sky-300/70"}`}>
<LayerThumbnail model={maskThumbnail} label={`${layer.name} mask thumbnail${editingMask ? ", currently editing" : ""}`} compact />
</span>
) : null}
</span>
{editingTitle?.type === "layer" && editingTitle.id === layer.id ? (
<RenameInput
value={editingTitle.draft}
@@ -298,9 +314,10 @@ function LayerRow({
</button>
</div>
{layerMask ? (
<div className="mt-2 flex min-h-14 flex-wrap items-center gap-2 rounded-[1.5rem] py-2 pl-4 pr-2 text-sm text-sky-100/70 hover:bg-white/[0.04]">
<Stack size={24} weight="fill" className="shrink-0" />
<span className="min-w-28 flex-1 truncate">{maskLayer ? "Layer mask" : "Layer mask missing"}</span>
<div role="group" aria-label={`Mask attached to ${layer.name}${editingMask ? ", currently editing" : ""}`} className={`relative mt-2 ml-10 flex min-h-14 flex-wrap items-center gap-2 rounded-[1.5rem] border-l-2 py-2 pl-4 pr-2 text-sm transition ${editingMask ? "border-sky-300 bg-sky-300/15 text-sky-50 ring-1 ring-inset ring-sky-300/30" : "border-sky-300/30 text-sky-100/70 hover:bg-white/[0.04]"}`}>
<span aria-hidden="true" className="absolute -left-2 top-1/2 h-px w-2 bg-sky-300/40" />
{maskThumbnail ? <LayerThumbnail model={maskThumbnail} label={`${layer.name} mask preview`} /> : <Stack size={24} weight="fill" className="shrink-0" />}
<span className="min-w-28 flex-1 truncate font-medium">{maskLayer ? editingMask ? "Editing layer mask" : "Layer mask" : "Layer mask missing"}</span>
{maskAsset ? <MaskStatus asset={maskAsset} /> : null}
{maskLayer ? (
<div className="flex flex-wrap items-center gap-1">
@@ -366,6 +383,7 @@ function LayerRow({
setEditingTitle={setEditingTitle}
maskLayerIds={maskLayerIds}
maskEdit={maskEdit}
thumbnailByLayerId={thumbnailByLayerId}
dispatch={dispatch}
/>
))