FN-8613: theme mailbox markdown links

Theme mailbox message anchors with dashboard tokens and verify link coverage.

- Style markdown anchor default, visited, hover, and focus-visible states with design tokens
- Cover inline, autolink, list, and table links while preserving file-path controls
- Add a patch changeset for themed mailbox links

Files changed:
 .changeset/theme-mailbox-markdown-links.md         |  7 ++++
 packages/dashboard/app/components/MailboxModal.css | 29 +++++++++++++
 .../__tests__/MailboxMessageContent.test.tsx       | 49 ++++++++++++++++++----
 3 files changed, 77 insertions(+), 8 deletions(-)

Fusion-Task-Id: FN-8613

Fusion-Task-Lineage: a8c7e8bb-8238-4018-bf95-38193f908eb4

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
gsxdsm
2026-07-26 14:49:17 -07:00
parent 07c8c95b10
commit 65f6748e5b
3 changed files with 77 additions and 8 deletions

View File

@@ -0,0 +1,7 @@
---
"@runfusion/fusion": patch
---
summary: Mailbox message links now use theme colors instead of default browser blue.
category: fix
dev: Markdown anchor states use dashboard tokens in shared MailboxMessageContent styling.

View File

@@ -411,6 +411,35 @@ Artifact-registration mail messages now render metadata-driven media and produci
padding-left: var(--space-lg); padding-left: var(--space-lg);
} }
/*
FNXC:Mailbox 2026-07-26-14:39:
Markdown links in mailbox message bodies must use theme tokens: browser default blue
and visited purple clash with dark themes and violate the dashboard token system.
*/
.mailbox-markdown a {
color: var(--accent);
text-decoration: underline;
text-decoration-color: color-mix(in srgb, var(--accent) 45%, transparent);
text-underline-offset: var(--space-2xs);
border-radius: var(--radius-sm);
overflow-wrap: anywhere;
transition: color var(--transition-fast), text-decoration-color var(--transition-fast);
}
.mailbox-markdown a:visited {
color: var(--accent);
}
.mailbox-markdown a:hover {
color: color-mix(in srgb, var(--accent) 85%, var(--text));
text-decoration-color: var(--accent);
}
.mailbox-markdown a:focus-visible {
outline: none;
box-shadow: var(--focus-ring-strong);
}
.mailbox-markdown :not(pre) > code { .mailbox-markdown :not(pre) > code {
padding: 0 var(--space-xs); padding: 0 var(--space-xs);
border-radius: var(--radius-sm); border-radius: var(--radius-sm);

View File

@@ -1,9 +1,13 @@
import { readFileSync } from "node:fs";
import { resolve } from "node:path";
import { describe, it, expect, afterEach, vi } from "vitest"; import { describe, it, expect, afterEach, vi } from "vitest";
import { render, cleanup, screen, waitFor } from "@testing-library/react"; import { render, cleanup, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event"; import userEvent from "@testing-library/user-event";
import { FileBrowserProvider } from "../../context/FileBrowserContext"; import { FileBrowserProvider } from "../../context/FileBrowserContext";
import { MailboxMessageContent } from "../MailboxMessageContent"; import { MailboxMessageContent } from "../MailboxMessageContent";
const mailboxModalCss = readFileSync(resolve(__dirname, "../MailboxModal.css"), "utf8");
// FNXC:Markdown 2026-06-23-03:15: Mock the heavy `mermaid` library so the mermaid // FNXC:Markdown 2026-06-23-03:15: Mock the heavy `mermaid` library so the mermaid
// rendering tests do not pull in the real parser/renderer bundle. The component // rendering tests do not pull in the real parser/renderer bundle. The component
// lazy-imports `mermaid` (default export), so we mock the module default. // lazy-imports `mermaid` (default export), so we mock the module default.
@@ -61,15 +65,44 @@ describe("MailboxMessageContent", () => {
expect(pre?.textContent).toContain("npm install"); expect(pre?.textContent).toContain("npm install");
}); });
it("renders links with target=_blank and noopener noreferrer", () => { it("themes markdown links consistently across inline, autolink, list, and table content", () => {
render( const content = [
<MailboxMessageContent content="See [docs](https://example.com/docs)." />, "See [inline docs](https://example.com/inline).",
"Visit https://example.com/autolink.",
"- [list docs](https://example.com/list)",
"",
"| docs |",
"| --- |",
"| [table docs](https://example.com/table) |",
"",
"See packages/dashboard/app/App.tsx for context.",
].join("\n");
const { container } = render(
<FileBrowserProvider openFile={vi.fn()}>
<MailboxMessageContent content={content} />
</FileBrowserProvider>,
); );
const link = screen.getByRole("link", { name: "docs" });
expect(link.getAttribute("href")).toBe("https://example.com/docs"); const links = screen.getAllByRole("link");
expect(link.getAttribute("target")).toBe("_blank"); expect(links).toHaveLength(4);
expect(link.getAttribute("rel")).toContain("noopener"); for (const link of links) {
expect(link.getAttribute("rel")).toContain("noreferrer"); expect(link.closest(".mailbox-markdown")).not.toBeNull();
expect(link).toHaveAttribute("target", "_blank");
expect(link).toHaveAttribute("rel", "noopener noreferrer");
}
expect(screen.getByRole("button", { name: "packages/dashboard/app/App.tsx" }))
.toHaveClass("file-path-link");
expect(container.querySelector("a.file-path-link")).toBeNull();
});
it("defines token-only mailbox markdown anchor styles", () => {
const anchorRule = mailboxModalCss.match(/\.mailbox-markdown a\s*\{([^}]*)\}/)?.[1];
expect(mailboxModalCss).toMatch(/\.mailbox-markdown a\b/);
expect(anchorRule).toBeDefined();
expect(anchorRule).not.toMatch(/#[0-9a-f]{3,8}\b|rgba\(/i);
expect(mailboxModalCss).toMatch(/\.mailbox-markdown a:visited\s*\{/);
expect(mailboxModalCss).toMatch(/\.mailbox-markdown a:hover\s*\{/);
expect(mailboxModalCss).toMatch(/\.mailbox-markdown a:focus-visible\s*\{/);
}); });
it("renders GFM tables with the mailbox-scoped class", () => { it("renders GFM tables with the mailbox-scoped class", () => {