mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-29 06:28:23 +02:00
* feat(views): unify avatar upload with crop editing across web/desktop Add a shared AvatarUploadControl + AvatarCropDialog used by the user, workspace, agent, and squad avatar entry points. Cropping (pan/zoom, fixed 1:1) and compression run client-side on canvas; the existing /api/upload-file + avatar_url chain is reused unchanged (no backend/API/DB changes). This collapses four hand-rolled upload buttons into one control and removes AvatarPicker. Also make the shared display avatar treat all non-human actors (agent, squad, system) as rounded squares — completing the "circles are for humans" convention the editors already assumed, so display and editors agree. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * feat(views): rebuild avatar cropper to the "Edit avatar" reference form Replace the hand-rolled canvas cropper with react-easy-crop to match the requested design: full-bleed image with a dimmed overlay outside a bright crop window, a rotate control, a zoom slider flanked by −/+, and a Reset / Cancel / Save footer. Round window for people, rounded-square for non-human actors; output stays a 512px square (webp, jpeg fallback) through the same upload/avatar_url chain. avatar-crop.ts keeps the encode pipeline and gains rotation-aware getCroppedAvatarBlob; the interactive geometry now lives in react-easy-crop. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * fix(views): round square avatar crop frame --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai>
39 lines
1.2 KiB
TypeScript
39 lines
1.2 KiB
TypeScript
import { describe, it, expect } from "vitest";
|
|
import { blobToAvatarFile } from "./avatar-crop";
|
|
|
|
describe("blobToAvatarFile", () => {
|
|
const blob = new Blob(["x"], { type: "image/webp" });
|
|
|
|
it("maps the output type to the matching extension", () => {
|
|
expect(blobToAvatarFile(blob, "photo.png", "image/webp").name).toBe(
|
|
"photo.webp",
|
|
);
|
|
expect(blobToAvatarFile(blob, "photo.png", "image/jpeg").name).toBe(
|
|
"photo.jpg",
|
|
);
|
|
expect(blobToAvatarFile(blob, "photo.gif", "image/png").name).toBe(
|
|
"photo.png",
|
|
);
|
|
});
|
|
|
|
it("strips only the final extension from the source name", () => {
|
|
expect(blobToAvatarFile(blob, "my.avatar.jpeg", "image/webp").name).toBe(
|
|
"my.avatar.webp",
|
|
);
|
|
expect(blobToAvatarFile(blob, "no-extension", "image/webp").name).toBe(
|
|
"no-extension.webp",
|
|
);
|
|
});
|
|
|
|
it("falls back to a stable base name when the source has none", () => {
|
|
expect(blobToAvatarFile(blob, "", "image/jpeg").name).toBe("avatar.jpg");
|
|
expect(blobToAvatarFile(blob, ".png", "image/jpeg").name).toBe("avatar.jpg");
|
|
});
|
|
|
|
it("carries the output mime type onto the File", () => {
|
|
expect(blobToAvatarFile(blob, "photo.png", "image/webp").type).toBe(
|
|
"image/webp",
|
|
);
|
|
});
|
|
});
|