the browser
+ // ignores it outright, and on a component nothing proves it is forwarded to
+ // an image — crediting either would exempt an unnamed link on the strength
+ // of inert markup.
+ expect(isNamed('
', 'a')).toBe(false);
+ expect(isNamed('
', 'a')).toBe(false);
+
+ // An anchor wrapping a component THIS FILE declares is named by what that
+ // component renders, so an aria-label here would replace a whole card's
+ // text with a terser name. The discriminator is text, not componentness:
+ // every link this rule exists to catch wraps an imported icon, and a
+ // file-local icon wrapper is credited no more than an imported one.
+ const localCard = 'function Card({ t }) {\n return (
{t.key}
);\n}\n';
+ const localIcon = 'function IconOnly() {\n return (
);\n}\n';
+ expect(isNamed(`${localCard}
`, 'a')).toBe(true);
+ expect(isNamed(`${localIcon}
`, 'a')).toBe(false);
+
+ // An aria-hidden child is removed from the name computation, so an anchor
+ // whose ONLY content is hidden computes an empty name — worse than the
+ // title-only shape, and invisible to a check that merely asked "does this
+ // anchor have children".
+ expect(isNamed('
', 'a')).toBe(false);
+ expect(isNamed('
', 'a')).toBe(false);
+ expect(isNamed('
→ Next page', 'a')).toBe(true);
+ });
+
it('masks JSX examples written in comments after an expression-rendered list', () => {
// maskComments exists so a `
` mentioned in prose isn't scanned as
// markup. One shape stranded it: an element rendered from inside an
diff --git a/client/src/components/brain/tabs/LinksTab.jsx b/client/src/components/brain/tabs/LinksTab.jsx
index b30b486a65..487c4f2a90 100644
--- a/client/src/components/brain/tabs/LinksTab.jsx
+++ b/client/src/components/brain/tabs/LinksTab.jsx
@@ -782,6 +782,7 @@ export default function LinksTab({ onRefresh }) {
draggable={false}
className="p-1.5 text-gray-400 hover:text-port-accent transition-colors"
title="Open in new tab"
+ aria-label={`Open ${link.title || link.url} in a new tab`}
>
diff --git a/client/src/components/media/MediaCard.jsx b/client/src/components/media/MediaCard.jsx
index 9e342e70c6..cd93f1ad40 100644
--- a/client/src/components/media/MediaCard.jsx
+++ b/client/src/components/media/MediaCard.jsx
@@ -233,6 +233,7 @@ function MediaCard({
download
className="shrink-0 px-1.5 py-1 bg-port-border hover:bg-port-border/70 text-white text-[10px] rounded flex items-center justify-center"
title="Download"
+ aria-label="Download"
>
diff --git a/client/src/components/media/MediaLightbox.jsx b/client/src/components/media/MediaLightbox.jsx
index 34e0aefb32..677094e032 100644
--- a/client/src/components/media/MediaLightbox.jsx
+++ b/client/src/components/media/MediaLightbox.jsx
@@ -920,6 +920,7 @@ function SettingsPane({
diff --git a/client/src/components/moodBoard/MoodBoardReferenceStrip.jsx b/client/src/components/moodBoard/MoodBoardReferenceStrip.jsx
index 0a4d298a50..3fc0166b12 100644
--- a/client/src/components/moodBoard/MoodBoardReferenceStrip.jsx
+++ b/client/src/components/moodBoard/MoodBoardReferenceStrip.jsx
@@ -200,6 +200,7 @@ export default function MoodBoardReferenceStrip({
target="_blank"
rel="noopener noreferrer"
title="Open board in new tab"
+ aria-label="Open board in new tab"
className="shrink-0 p-1 text-gray-400 hover:text-port-accent"
>
diff --git a/client/src/pages/Browser.jsx b/client/src/pages/Browser.jsx
index e19e15e347..61edaad53c 100644
--- a/client/src/pages/Browser.jsx
+++ b/client/src/pages/Browser.jsx
@@ -591,6 +591,7 @@ export default function BrowserPage() {
download={file.name}
className="p-1.5 rounded-md text-gray-400 hover:text-white hover:bg-port-border/50 transition-colors"
title="Save to device"
+ aria-label={`Save ${file.name} to device`}
>
diff --git a/client/src/pages/Loras.jsx b/client/src/pages/Loras.jsx
index 0923e6cafc..c3cbbe63ed 100644
--- a/client/src/pages/Loras.jsx
+++ b/client/src/pages/Loras.jsx
@@ -1257,6 +1257,7 @@ function LoraCard({ lora, onDelete, onMeasured, deleting, deleteConfirm }) {
rel="noopener noreferrer"
className="text-gray-400 hover:text-gray-200 p-1.5 rounded hover:bg-port-bg"
title="Open on Civitai"
+ aria-label="Open on Civitai"
>
@@ -1268,6 +1269,7 @@ function LoraCard({ lora, onDelete, onMeasured, deleting, deleteConfirm }) {
rel="noopener noreferrer"
className="text-gray-400 hover:text-gray-200 p-1.5 rounded hover:bg-port-bg"
title="Open on HuggingFace"
+ aria-label="Open on HuggingFace"
>
diff --git a/client/src/pages/Uploads.jsx b/client/src/pages/Uploads.jsx
index 6036f90ad5..02d6be04ec 100644
--- a/client/src/pages/Uploads.jsx
+++ b/client/src/pages/Uploads.jsx
@@ -266,6 +266,7 @@ export default function Uploads() {
rel="noopener noreferrer"
className="p-2 text-gray-500 hover:text-port-accent transition-colors"
title="Download / View"
+ aria-label={`Download or view ${file.filename}`}
>