Skip to content
Open
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
39 changes: 39 additions & 0 deletions src/components/prompts/AiToolBadge.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { render, screen, fireEvent } from "@testing-library/react";
import { describe, it, expect } from "vitest";
import { AiToolBadge } from "./AiToolBadge";

describe("AiToolBadge", () => {
it("renders abbreviated label initially with full tool name in title and aria-label", () => {
render(<AiToolBadge tool="Stable Diffusion" />);

const badge = screen.getByRole("button", { name: "AI tool: Stable Diffusion" });
expect(badge).toBeInTheDocument();
expect(badge).toHaveAttribute("title", "Stable Diffusion");
expect(badge).toHaveTextContent("SD");
});

it("toggles to full tool name on tap/click without needing hover/tooltip", () => {
render(<AiToolBadge tool="NANO BANANA (Gemini)" />);

const badge = screen.getByRole("button", { name: "AI tool: NANO BANANA (Gemini)" });
expect(badge).toHaveTextContent("Nano Banana");

// Tap/click to reveal full name
fireEvent.click(badge);
expect(badge).toHaveTextContent("NANO BANANA (Gemini)");
expect(badge).toHaveAttribute("aria-expanded", "true");

// Tap again to toggle back
fireEvent.click(badge);
expect(badge).toHaveTextContent("Nano Banana");
expect(badge).toHaveAttribute("aria-expanded", "false");
});

it("renders custom tool names with fallback gracefully", () => {
render(<AiToolBadge tool="Custom AI Engine" />);

const badge = screen.getByRole("button", { name: "AI tool: Custom AI Engine" });
expect(badge).toBeInTheDocument();
expect(badge).toHaveTextContent("Custom AI Engine");
});
});
31 changes: 25 additions & 6 deletions src/components/prompts/AiToolBadge.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { useState } from "react";
import { Sparkles } from "lucide-react";
import { cn } from "@/lib/utils";
import { AdobeIcon, GeminiIcon, MetaIcon, OpenAIIcon } from "./toolIcons";
Expand Down Expand Up @@ -45,21 +46,39 @@ interface AiToolBadgeProps {
}

export function AiToolBadge({ tool, className }: AiToolBadgeProps) {
const [showFull, setShowFull] = useState(false);
// "Other" lets people type anything, so an unknown name is expected, not a bug.
const { label, icon: Icon, emoji } = PRESENTATION[tool] ?? { label: tool, icon: Sparkles };
const displayText = showFull ? tool : label;

return (
// The full name stays in the tooltip — the label is abbreviated, and on a
// card there is no room to spell out "NANO BANANA (Gemini)".
<span className={cn("flex items-center gap-1 min-w-0", className)} title={tool}>
// The full name is findable via title tooltip on desktop, via tap/click
// toggle on touch devices, and via aria-label for assistive tech.
<button
type="button"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
setShowFull((prev) => !prev);
}}
className={cn(
"flex items-center gap-1 min-w-0 text-left cursor-pointer hover:text-foreground transition-colors touch-target",
className
)}
title={tool}
aria-label={`AI tool: ${tool}`}
aria-expanded={showFull}
>
{emoji ? (
<span aria-hidden="true" className="flex-shrink-0 text-[13px] leading-none">
<span aria-hidden="true" className="flex-shrink-0 text-xs leading-none">
{emoji}
</span>
) : (
Icon && <Icon className="h-3.5 w-3.5 flex-shrink-0" />
)}
<span className="truncate">{label}</span>
</span>
<span className={showFull ? "whitespace-normal break-words" : "truncate"}>
{displayText}
</span>
</button>
);
}
40 changes: 40 additions & 0 deletions src/components/prompts/PromptCard.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,46 @@ describe("PromptCard", () => {
expect(ratingElement).toHaveTextContent("Not rated");
});

it("renders mobile drawer with complete actions including Copy Prompt and Like", () => {
renderPromptCard(baseProps);

const [mobileTrigger] = screen.getAllByLabelText("More options");
fireEvent.click(mobileTrigger);

// Mobile drawer contains all primary actions
expect(screen.getByRole("button", { name: "Copy" })).toBeInTheDocument();
expect(screen.getByRole("button", { name: "Like" })).toBeInTheDocument();
expect(screen.getByRole("button", { name: "Save" })).toBeInTheDocument();
expect(screen.getByRole("button", { name: "Share" })).toBeInTheDocument();
expect(screen.getByRole("button", { name: "Link" })).toBeInTheDocument();
expect(screen.getByText("View Profile")).toBeInTheDocument();
expect(screen.getByText("AI Tool:")).toBeInTheDocument();
expect(screen.getAllByText("Midjourney").length).toBeGreaterThan(0);
});

it("renders desktop dropdown with complete actions including Copy Prompt and Like", () => {
renderPromptCard(baseProps);

const [, desktopTrigger] = screen.getAllByLabelText("More options");
fireEvent.pointerDown(desktopTrigger, { button: 0, ctrlKey: false });
fireEvent.keyDown(desktopTrigger, { key: "ArrowDown" });

expect(screen.getByText("Copy Prompt")).toBeInTheDocument();
expect(screen.getByText("Like")).toBeInTheDocument();
expect(screen.getByText("Save")).toBeInTheDocument();
expect(screen.getByText("Share")).toBeInTheDocument();
expect(screen.getByText("Copy Link")).toBeInTheDocument();
expect(screen.getByText("View Profile")).toBeInTheDocument();
expect(screen.getByText("Tool:")).toBeInTheDocument();
});

it("includes touch hover-none override on copy button for touch screens like iPad Pro (#45)", () => {
renderPromptCard(baseProps);

const copyButton = screen.getByLabelText("Copy prompt");
expect(copyButton.className).toContain("[@media(hover:none)]:!opacity-100");
});

it("applies snappy transition tokens to card and image", () => {
renderPromptCard(baseProps);

Expand Down
Loading
Loading