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
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { type Image } from "@tissuumaps/core";

import { cn } from "@/lib/utils";

import { Fieldset, FieldsetLegend } from "../../common/fieldset";

export type ImagesLayersPanelProps = {
image: Image;
className?: string;
};

export function ImagesLayersPanel({ className }: ImagesLayersPanelProps) {
return (
<Fieldset
className={cn("flex flex-col gap-y-2 border rounded-md p-2", className)}
>
<FieldsetLegend className="font-medium text-foreground">
Layers
</FieldsetLegend>
</Fieldset>
);
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { type Image } from "@tissuumaps/core";

import { Input } from "@/components/ui/input";
import { Switch } from "@/components/ui/switch";
import { cn } from "@/lib/utils";

import { useTissUUmaps } from "../../../store";
import { Field, FieldLabel } from "../../common/field";
import { Fieldset, FieldsetLegend } from "../../common/fieldset";

export type ImagesSettingsPanelProps = {
image: Image;
className?: string;
};

export function ImagesSettingsPanel({
image,
className,
}: ImagesSettingsPanelProps) {
const updateImage = useTissUUmaps((state) => state.updateImage);

return (
<Fieldset
className={cn("flex flex-col gap-y-2 border rounded-md p-2", className)}
>
<FieldsetLegend className="font-medium text-foreground">
Settings
</FieldsetLegend>
<Field>
<FieldLabel>Name</FieldLabel>
<Input
value={image.name}
onChange={(event) =>
updateImage(image.id, { name: event.target.value })
}
/>
</Field>
<Field>
<FieldLabel>Visibility</FieldLabel>
<div className="flex flex-row items-center gap-x-2">
<Switch
checked={image.visibility}
onCheckedChange={(checked) =>
updateImage(image.id, { visibility: checked })
}
/>
{image.visibility ? "Visible" : "Hidden"}
</div>
</Field>
<Field>
<FieldLabel>Opacity</FieldLabel>
<Input
type="number"
min={0}
max={1}
step={0.01}
value={image.opacity}
onChange={(event) => {
const opacity = event.target.valueAsNumber;
if (Number.isFinite(opacity)) {
updateImage(image.id, {
opacity: Math.min(Math.max(0, opacity), 1),
});
}
}}
/>
</Field>
</Fieldset>
);
}
Original file line number Diff line number Diff line change
@@ -1,22 +1,29 @@
import { JsonForms } from "@jsonforms/react";
import { EditIcon } from "lucide-react";
import { useMemo } from "react";

import { type Image, type ImageDataSource } from "@tissuumaps/core";
import { type Image } from "@tissuumaps/core";

import { Input } from "@/components/ui/input";
import { cn } from "@/lib/utils";

import { useTissUUmaps } from "../../../store";
import { Field, FieldLabel } from "../../common/field";
import { Fieldset, FieldsetLegend } from "../../common/fieldset";
import { cells, renderers } from "../../jsonforms";

export type ImagesPanelItemSettingsProps = {
export type ImagesSourcePanelProps = {
image: Image;
className?: string;
};

export function ImagesPanelItemSettings({
export function ImagesSourcePanel({
image,
}: ImagesPanelItemSettingsProps) {
className,
}: ImagesSourcePanelProps) {
const imageDataStorageRegistry = useTissUUmaps(
(state) => state.imageDataStorageRegistry,
);
const updateImage = useTissUUmaps((state) => state.updateImage);

const { dataSourceSchema, dataSourceUISchema } = useMemo(() => {
const value = imageDataStorageRegistry.get(image.dataSource.type);
Expand All @@ -29,25 +36,25 @@ export function ImagesPanelItemSettings({
}, [imageDataStorageRegistry, image.dataSource.type]);

return (
<div>
{/* Data source */}
<Fieldset
className={cn("flex flex-col gap-y-2 border rounded-md p-2", className)}
>
<FieldsetLegend className="flex flex-row items-center font-medium text-foreground">
Source
<EditIcon className="ml-auto size-4" />
</FieldsetLegend>
<Field>
<FieldLabel>Type</FieldLabel>
<Input type="text" value={image.dataSource.type} disabled />
</Field>
<JsonForms
schema={dataSourceSchema}
uischema={dataSourceUISchema}
data={image.dataSource}
onChange={({ data, errors }) => {
if (errors === undefined || errors.length === 0) {
updateImage(image.id, {
dataSource: {
...image.dataSource,
...(data as ImageDataSource),
},
});
}
}}
renderers={renderers}
cells={cells}
readonly={true}
/>
</div>
</Fieldset>
);
}
86 changes: 74 additions & 12 deletions apps/tissuumaps/src/components/panels/ImagesPanel/index.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
import { DragDropProvider } from "@dnd-kit/react";
import { isSortable, useSortable } from "@dnd-kit/react/sortable";
import { GripVertical } from "lucide-react";
import { EyeIcon, EyeOffIcon, GripVertical, Trash2Icon } from "lucide-react";

import { type Image } from "@tissuumaps/core";

import { Button } from "@/components/ui/button";
import {
InputGroup,
InputGroupAddon,
InputGroupInput,
} from "@/components/ui/input-group";

import { useTissUUmaps } from "../../../store";
import {
Accordion,
Expand All @@ -13,7 +20,9 @@ import {
AccordionTrigger,
AccordionTriggerUpDownIcon,
} from "../../common/accordion";
import { ImagesPanelItem } from "./ImagesPanelItem";
import { ImagesLayersPanel } from "./ImagesLayersPanel";
import { ImagesSettingsPanel } from "./ImagesSettingsPanel";
import { ImagesSourcePanel } from "./ImagesSourcePanel";

export type ImagesPanelProps = {
className?: string;
Expand Down Expand Up @@ -49,18 +58,71 @@ type ImageAccordionItemProps = {
};

function ImageAccordionItem({ image, index }: ImageAccordionItemProps) {
const updateImage = useTissUUmaps((state) => state.updateImage);
const deleteImage = useTissUUmaps((state) => state.deleteImage);

const { ref, handleRef } = useSortable({ id: image.id, index });

return (
<AccordionItem render={<div ref={ref} />}>
<AccordionHeader>
<GripVertical ref={handleRef} />
<AccordionTrigger>{image.name}</AccordionTrigger>
<AccordionTriggerUpDownIcon className="ml-auto" />
</AccordionHeader>
<AccordionPanel>
<ImagesPanelItem image={image} />
</AccordionPanel>
</AccordionItem>
<div ref={ref}>
<AccordionItem className="border rounded-md bg-sidebar p-2">
<AccordionHeader>
<GripVertical ref={handleRef} />
<div className="flex-1 w-full">
<AccordionTrigger className="w-full cursor-pointer">
{image.name}
</AccordionTrigger>
</div>
<div className="ml-auto flex flex-row items-center gap-x-2">
<InputGroup className="w-24">
<InputGroupAddon>OPA</InputGroupAddon>
<InputGroupInput
type="number"
min={0}
max={1}
step={0.01}
value={image.opacity}
onChange={(event) => {
const opacity = event.target.valueAsNumber;
if (Number.isFinite(opacity)) {
updateImage(image.id, {
opacity: Math.min(Math.max(0, opacity), 1),
});
}
}}
/>
</InputGroup>
<Button
variant="ghost"
onClick={() =>
updateImage(image.id, { visibility: !image.visibility })
}
>
{image.visibility ? <EyeIcon /> : <EyeOffIcon />}
</Button>
Comment on lines +95 to +102
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These icon-only buttons (visibility toggle, and similarly the delete icon button) should have an accessible name (e.g., aria-label and/or a visually-hidden text node). Relying on the icon alone (or on a title attribute) is not sufficient for screen readers.

Copilot uses AI. Check for mistakes.
<Button
variant="ghost"
onClick={() => {
// TODO replace by dialog overlay
if (
window.confirm("Are you sure you want to delete this image?")
) {
deleteImage(image.id);
}
}}
title="Delete image"
>
<Trash2Icon />
</Button>
</div>
<AccordionTriggerUpDownIcon />
</AccordionHeader>
<AccordionPanel className="pt-2 flex flex-col gap-y-2">
<ImagesSourcePanel image={image} className="bg-card" />
<ImagesSettingsPanel image={image} className="bg-card" />
<ImagesLayersPanel image={image} className="bg-card" />
</AccordionPanel>
</AccordionItem>
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { type Labels } from "@tissuumaps/core";

import { cn } from "@/lib/utils";

import { Fieldset, FieldsetLegend } from "../../common/fieldset";

export type LabelsLayersPanelProps = {
labels: Labels;
className?: string;
};

export function LabelsLayersPanel({ className }: LabelsLayersPanelProps) {
return (
<Fieldset
className={cn("flex flex-col gap-y-2 border rounded-md p-2", className)}
>
<FieldsetLegend className="font-medium text-foreground">
Layers
</FieldsetLegend>
</Fieldset>
);
}

This file was deleted.

Loading
Loading