feat: support multiple image uploads for AI event generation 🖼️
- Updated OpenRouter integration to accept an array of image URLs - Updated ImagePicker to use the `multiple` attribute natively - Added `appendImagesDeduped` for handling client-side image deduplication - Enhanced clipboard pasting to extract multiple images at once - Rendered multiple images in a horizontal thumbnail strip in the AIToolbar - Added tests to cover multi-image logic and AI request mapping
This commit is contained in:
@@ -217,6 +217,69 @@ describe("Keyboard shortcuts – toolbar integration contract", () => {
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Cycle 8: Multi-image thumbnail strip ────────────────────────────────────
|
||||
//
|
||||
// When multiple images are attached, they render as a horizontal scrollable
|
||||
// strip of 64×64 thumbnails below the textarea.
|
||||
//
|
||||
// Contract:
|
||||
// - Strip wrapper: `flex` + `overflow-x-auto` so it scrolls horizontally
|
||||
// - Each thumbnail wrapper: `relative inline-block` so the X button can be
|
||||
// positioned absolutely on top
|
||||
// - Image itself: fixed 64×64, `object-cover`
|
||||
// - Remove button: `absolute`, positioned at top-right corner
|
||||
|
||||
const IMAGE_STRIP_CLASSES = "flex gap-2 overflow-x-auto py-1";
|
||||
const THUMBNAIL_WRAPPER_CLASSES = "relative inline-block shrink-0";
|
||||
const THUMBNAIL_IMAGE_CLASSES = "h-16 w-16 rounded-md object-cover";
|
||||
const THUMBNAIL_REMOVE_BTN_CLASSES = "absolute -top-1.5 -right-1.5";
|
||||
|
||||
describe("Multi-image strip – layout contract", () => {
|
||||
test("image strip wrapper uses flex layout for horizontal row", () => {
|
||||
const resolved = cn(IMAGE_STRIP_CLASSES);
|
||||
expect(resolved).toContain("flex");
|
||||
});
|
||||
|
||||
test("image strip wrapper has overflow-x-auto for horizontal scroll when many images", () => {
|
||||
const resolved = cn(IMAGE_STRIP_CLASSES);
|
||||
expect(resolved).toContain("overflow-x-auto");
|
||||
});
|
||||
|
||||
test("image strip wrapper has gap between thumbnails", () => {
|
||||
const resolved = cn(IMAGE_STRIP_CLASSES);
|
||||
expect(resolved).toMatch(/\bgap-[1-9]\d*\b/);
|
||||
});
|
||||
|
||||
test("thumbnail wrapper is relative+inline-block so the remove button can be positioned absolutely", () => {
|
||||
const resolved = cn(THUMBNAIL_WRAPPER_CLASSES);
|
||||
expect(resolved).toContain("relative");
|
||||
expect(resolved).toContain("inline-block");
|
||||
});
|
||||
|
||||
test("thumbnail wrapper does not shrink (shrink-0) so images keep their size in flex row", () => {
|
||||
const resolved = cn(THUMBNAIL_WRAPPER_CLASSES);
|
||||
expect(resolved).toContain("shrink-0");
|
||||
});
|
||||
|
||||
test("thumbnail image has fixed 64×64 size (h-16 w-16)", () => {
|
||||
const resolved = cn(THUMBNAIL_IMAGE_CLASSES);
|
||||
expect(resolved).toContain("h-16");
|
||||
expect(resolved).toContain("w-16");
|
||||
});
|
||||
|
||||
test("thumbnail image uses object-cover so it crops without distortion", () => {
|
||||
const resolved = cn(THUMBNAIL_IMAGE_CLASSES);
|
||||
expect(resolved).toContain("object-cover");
|
||||
});
|
||||
|
||||
test("remove button is positioned absolutely at top-right corner of the thumbnail", () => {
|
||||
const resolved = cn(THUMBNAIL_REMOVE_BTN_CLASSES);
|
||||
expect(resolved).toContain("absolute");
|
||||
expect(resolved).toMatch(/-top-/);
|
||||
expect(resolved).toMatch(/-right-/);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Cycle 5: Textarea AI prompt – spacing contract (existing behavior) ──────
|
||||
|
||||
describe("AI textarea – prompt input spacing contract", () => {
|
||||
|
||||
Reference in New Issue
Block a user