diff --git a/app/features/art/components/ArtGrid.module.css b/app/features/art/components/ArtGrid.module.css
index 42321780f..0393bf380 100644
--- a/app/features/art/components/ArtGrid.module.css
+++ b/app/features/art/components/ArtGrid.module.css
@@ -1,3 +1,15 @@
+.thumbnailButton {
+ all: unset;
+ display: block;
+ cursor: pointer;
+
+ /* thumbnails have square corners, a rounded ring would leave the art's corners outside it */
+ &:focus-visible {
+ outline: var(--focus-ring);
+ outline-offset: 2px;
+ }
+}
+
.thumbnail {
cursor: pointer;
transition: all 0.2s ease;
diff --git a/app/features/art/components/ArtGrid.tsx b/app/features/art/components/ArtGrid.tsx
index 401e2b6bf..6bfad46b9 100644
--- a/app/features/art/components/ArtGrid.tsx
+++ b/app/features/art/components/ArtGrid.tsx
@@ -211,20 +211,31 @@ function ImagePreview({
const { t } = useTranslation(["common", "art"]);
const formatDistanceToNow = useFormatDistanceToNow();
- const img = (
- // biome-ignore lint/a11y/noStaticElementInteractions: Biome v2 migration
+ const image = (
preloadImage(art.url) : undefined}
ref={imageRef}
className={enablePreview ? styles.thumbnail : undefined}
data-testid="art-image"
/>
);
+ const img = onClick ? (
+
+ ) : (
+ image
+ );
+
if (!art.author && canEdit) {
return (
diff --git a/app/features/comp-analyzer/routes/comp-analyzer.tsx b/app/features/comp-analyzer/routes/comp-analyzer.tsx
index 2b08eda31..d4486cd2a 100644
--- a/app/features/comp-analyzer/routes/comp-analyzer.tsx
+++ b/app/features/comp-analyzer/routes/comp-analyzer.tsx
@@ -2,7 +2,11 @@ import { HardDriveDownload } from "lucide-react";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import type { MetaFunction, ShouldRevalidateFunction } from "react-router";
-import { SendouButton } from "~/components/elements/Button";
+import {
+ SendouButton,
+ type SendouButtonProps,
+} from "~/components/elements/Button";
+import { SendouPopover } from "~/components/elements/Popover";
import { SendouSwitch } from "~/components/elements/Switch";
import { Main } from "~/components/Main";
import { Placeholder } from "~/components/Placeholder";
@@ -63,6 +67,7 @@ export default function CompAnalyzerShell() {
}
function CompAnalyzerPage() {
+ const { t } = useTranslation(["common", "analyzer"]);
const [selectedWeaponIds, setSelectedWeaponIds] = useSelectedWeapons();
const [categorization, setCategorization] = useCategorization();
const [isGridCollapsed, setIsGridCollapsed] = useState(
@@ -99,14 +104,18 @@ function CompAnalyzerPage() {
onRemove={handleRemoveWeapon}
onReorder={setSelectedWeaponIds}
/>
- {selectedWeaponIds.length >= MAX_WEAPONS ? (
-
+
+ {selectedWeaponIds.length >= MAX_WEAPONS ? (
-
- ) : null}
+ ) : (
+
}>
+ {t("analyzer:comp.exportHint", { max: MAX_WEAPONS })}
+
+ )}
+
}
- >
- {t("common:imageExport.export")}
-
- }
+ trigger={
}
heading={t("common:imageExport.export")}
filename="comp"
settings={
@@ -212,3 +213,18 @@ function CompGraphicWithCombos({
/>
);
}
+
+function ExportButton(props: SendouButtonProps) {
+ const { t } = useTranslation(["common"]);
+
+ return (
+
}
+ >
+ {t("common:imageExport.export")}
+
+ );
+}
diff --git a/app/features/tier-list-maker/tier-list-maker-utils.test.ts b/app/features/tier-list-maker/tier-list-maker-utils.test.ts
index 0ffe7fa1d..74b4d0758 100644
--- a/app/features/tier-list-maker/tier-list-maker-utils.test.ts
+++ b/app/features/tier-list-maker/tier-list-maker-utils.test.ts
@@ -203,7 +203,10 @@ describe("isLightColor", () => {
{ hex: "#8b0000", expected: false, why: "dark red" },
{ hex: "#4169e1", expected: false, why: "royal blue" },
{ hex: "#000000", expected: false, why: "black" },
- { hex: "#fff", expected: false, why: "unsupported short form" },
+ { hex: "#fff", expected: true, why: "short form white" },
+ { hex: "#000", expected: false, why: "short form black" },
+ { hex: "#ffd", expected: true, why: "short form light yellow" },
+ { hex: "not-a-color", expected: false, why: "garbage" },
])("$why -> $expected", ({ hex, expected }) => {
expect(isLightColor(hex)).toBe(expected);
});
diff --git a/app/features/tier-list-maker/tier-list-maker-utils.ts b/app/features/tier-list-maker/tier-list-maker-utils.ts
index 887bdb55a..9d2d1f391 100644
--- a/app/features/tier-list-maker/tier-list-maker-utils.ts
+++ b/app/features/tier-list-maker/tier-list-maker-utils.ts
@@ -87,20 +87,36 @@ export function tierNameFontSize(name: string) {
const LIGHT_COLOR_LUMINANCE_THRESHOLD = 0.5;
-/** Whether dark text/icons read better on the given `#rrggbb` color than light ones. */
+/** Whether dark text/icons read better on the given `#rgb` or `#rrggbb` color than light ones. */
export function isLightColor(hex: string) {
- const channels = hex.replace("#", "").match(/.{2}/g);
- if (!channels || channels.length < 3) return false;
+ const channels = hexChannels(hex);
+ if (!channels) return false;
- const [r, g, b] = channels.map((channel) => Number.parseInt(channel, 16));
+ const [r, g, b] = channels;
const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
return luminance > LIGHT_COLOR_LUMINANCE_THRESHOLD;
}
-/** Text color that stays readable on a tier's `#rrggbb` background color. */
+/** Text color that stays readable on a tier's `#rgb` or `#rrggbb` background color. */
export function tierTextColor(hex: string) {
return isLightColor(hex)
? "var(--color-text-on-light)"
: "var(--color-text-on-dark)";
}
+
+function hexChannels(hex: string) {
+ const digits = hex.replace("#", "");
+ const channels =
+ digits.length === 3
+ ? digits.split("").map((digit) => `${digit}${digit}`)
+ : digits.match(/.{2}/g);
+
+ if (!channels || channels.length < 3) return null;
+
+ const parsed = channels
+ .slice(0, 3)
+ .map((channel) => Number.parseInt(channel, 16));
+
+ return parsed.some(Number.isNaN) ? null : parsed;
+}
diff --git a/locales/da/analyzer.json b/locales/da/analyzer.json
index ac183de84..9553b48c2 100644
--- a/locales/da/analyzer.json
+++ b/locales/da/analyzer.json
@@ -219,5 +219,6 @@
"comp.removeAll": "",
"comp.addAll": "",
"comp.singleWeaponCombos": "",
- "comp.graphicSubtitle": ""
+ "comp.graphicSubtitle": "",
+ "comp.exportHint": ""
}
diff --git a/locales/da/art.json b/locales/da/art.json
index b5349fc3d..d2bc3b231 100644
--- a/locales/da/art.json
+++ b/locales/da/art.json
@@ -23,5 +23,6 @@
"delete.title": "",
"unlink.title": "",
"noArtForTag": "",
- "imageFailed": ""
+ "imageFailed": "",
+ "openImage": ""
}
diff --git a/locales/de/analyzer.json b/locales/de/analyzer.json
index b62d5b6eb..b02045949 100644
--- a/locales/de/analyzer.json
+++ b/locales/de/analyzer.json
@@ -219,5 +219,6 @@
"comp.removeAll": "",
"comp.addAll": "",
"comp.singleWeaponCombos": "",
- "comp.graphicSubtitle": ""
+ "comp.graphicSubtitle": "",
+ "comp.exportHint": ""
}
diff --git a/locales/de/art.json b/locales/de/art.json
index 852565d8a..4d3d4b4d7 100644
--- a/locales/de/art.json
+++ b/locales/de/art.json
@@ -23,5 +23,6 @@
"delete.title": "",
"unlink.title": "",
"noArtForTag": "",
- "imageFailed": ""
+ "imageFailed": "",
+ "openImage": ""
}
diff --git a/locales/en/analyzer.json b/locales/en/analyzer.json
index 25cc78dd3..faf96e554 100644
--- a/locales/en/analyzer.json
+++ b/locales/en/analyzer.json
@@ -219,5 +219,6 @@
"comp.removeAll": "Remove all",
"comp.addAll": "Add all",
"comp.singleWeaponCombos": "Single weapon combos",
- "comp.graphicSubtitle": "Composition Analyzer"
+ "comp.graphicSubtitle": "Composition Analyzer",
+ "comp.exportHint": "Pick {{max}} weapons to export an image"
}
diff --git a/locales/en/art.json b/locales/en/art.json
index 655b8117d..6c931915a 100644
--- a/locales/en/art.json
+++ b/locales/en/art.json
@@ -23,5 +23,6 @@
"delete.title": "Are you sure you want to delete the art?",
"unlink.title": "Are you sure you want to remove this art from your profile (only {{username}} can add it back)?",
"noArtForTag": "No uploaded art found for #{{tag}}",
- "imageFailed": "Image could not be loaded"
+ "imageFailed": "Image could not be loaded",
+ "openImage": "Open image"
}
diff --git a/locales/es-ES/analyzer.json b/locales/es-ES/analyzer.json
index c59003008..10803581d 100644
--- a/locales/es-ES/analyzer.json
+++ b/locales/es-ES/analyzer.json
@@ -223,5 +223,6 @@
"comp.removeAll": "Eliminar todos",
"comp.addAll": "Añadir todos",
"comp.singleWeaponCombos": "Combos de un arma",
- "comp.graphicSubtitle": ""
+ "comp.graphicSubtitle": "",
+ "comp.exportHint": ""
}
diff --git a/locales/es-ES/art.json b/locales/es-ES/art.json
index a83b4598f..e335fbc27 100644
--- a/locales/es-ES/art.json
+++ b/locales/es-ES/art.json
@@ -24,5 +24,6 @@
"delete.title": "¿Seguro que quieres eliminar este arte?",
"unlink.title": "¿Seguro que quieres desvincular este arte de tu perfil? (solo {{username}} puede volver a añadirlo)",
"noArtForTag": "No se ha encontrado arte subido para #{{tag}}",
- "imageFailed": ""
+ "imageFailed": "",
+ "openImage": ""
}
diff --git a/locales/es-US/analyzer.json b/locales/es-US/analyzer.json
index 996ac0c53..36e3ca987 100644
--- a/locales/es-US/analyzer.json
+++ b/locales/es-US/analyzer.json
@@ -223,5 +223,6 @@
"comp.removeAll": "Eliminar todos",
"comp.addAll": "Añadir todos",
"comp.singleWeaponCombos": "Combos de un arma",
- "comp.graphicSubtitle": ""
+ "comp.graphicSubtitle": "",
+ "comp.exportHint": ""
}
diff --git a/locales/es-US/art.json b/locales/es-US/art.json
index f1756fdb5..56d215d0c 100644
--- a/locales/es-US/art.json
+++ b/locales/es-US/art.json
@@ -24,5 +24,6 @@
"delete.title": "¿Seguro que quieres eliminar este arte?",
"unlink.title": "¿Seguro que quieres desvincular este arte de tu perfil? (solo {{username}} puede volver a añadirlo)",
"noArtForTag": "No se ha encontrado arte subido para #{{tag}}",
- "imageFailed": ""
+ "imageFailed": "",
+ "openImage": ""
}
diff --git a/locales/fr-CA/analyzer.json b/locales/fr-CA/analyzer.json
index 12388804b..4268422ae 100644
--- a/locales/fr-CA/analyzer.json
+++ b/locales/fr-CA/analyzer.json
@@ -223,5 +223,6 @@
"comp.removeAll": "",
"comp.addAll": "",
"comp.singleWeaponCombos": "",
- "comp.graphicSubtitle": ""
+ "comp.graphicSubtitle": "",
+ "comp.exportHint": ""
}
diff --git a/locales/fr-CA/art.json b/locales/fr-CA/art.json
index 9f38265a1..543ad1b29 100644
--- a/locales/fr-CA/art.json
+++ b/locales/fr-CA/art.json
@@ -24,5 +24,6 @@
"delete.title": "",
"unlink.title": "",
"noArtForTag": "",
- "imageFailed": ""
+ "imageFailed": "",
+ "openImage": ""
}
diff --git a/locales/fr-EU/analyzer.json b/locales/fr-EU/analyzer.json
index 01c689472..bb5486f82 100644
--- a/locales/fr-EU/analyzer.json
+++ b/locales/fr-EU/analyzer.json
@@ -223,5 +223,6 @@
"comp.removeAll": "",
"comp.addAll": "",
"comp.singleWeaponCombos": "",
- "comp.graphicSubtitle": ""
+ "comp.graphicSubtitle": "",
+ "comp.exportHint": ""
}
diff --git a/locales/fr-EU/art.json b/locales/fr-EU/art.json
index 10328c217..8a73b5492 100644
--- a/locales/fr-EU/art.json
+++ b/locales/fr-EU/art.json
@@ -24,5 +24,6 @@
"delete.title": "Êtes vous sûre de vouloir supprimer cette œuvre?",
"unlink.title": "Êtes vous sûre de vouloir supprimer cette œuvre de votre profil? (Seulement {{username}} pourra le remettre)",
"noArtForTag": "",
- "imageFailed": ""
+ "imageFailed": "",
+ "openImage": ""
}
diff --git a/locales/he/analyzer.json b/locales/he/analyzer.json
index f5d919788..a6e86984f 100644
--- a/locales/he/analyzer.json
+++ b/locales/he/analyzer.json
@@ -223,5 +223,6 @@
"comp.removeAll": "הסר הכל",
"comp.addAll": "הוסף הכל",
"comp.singleWeaponCombos": "",
- "comp.graphicSubtitle": ""
+ "comp.graphicSubtitle": "",
+ "comp.exportHint": ""
}
diff --git a/locales/he/art.json b/locales/he/art.json
index 355a72d8e..294c25582 100644
--- a/locales/he/art.json
+++ b/locales/he/art.json
@@ -24,5 +24,6 @@
"delete.title": "האם אתה בטוח שאתה רוצה למחוק את הציור?",
"unlink.title": "האם אתה בטוח שאתה רוצה להסיר את הציור הזה מהפרופיל שלך (רק {{username}} יוכל להוסיף אותו חזרה)?",
"noArtForTag": "",
- "imageFailed": ""
+ "imageFailed": "",
+ "openImage": ""
}
diff --git a/locales/it/analyzer.json b/locales/it/analyzer.json
index 647ecedaf..4b8ec69e5 100644
--- a/locales/it/analyzer.json
+++ b/locales/it/analyzer.json
@@ -223,5 +223,6 @@
"comp.removeAll": "",
"comp.addAll": "",
"comp.singleWeaponCombos": "",
- "comp.graphicSubtitle": ""
+ "comp.graphicSubtitle": "",
+ "comp.exportHint": ""
}
diff --git a/locales/it/art.json b/locales/it/art.json
index bab969f02..13878e8f8 100644
--- a/locales/it/art.json
+++ b/locales/it/art.json
@@ -24,5 +24,6 @@
"delete.title": "",
"unlink.title": "",
"noArtForTag": "",
- "imageFailed": ""
+ "imageFailed": "",
+ "openImage": ""
}
diff --git a/locales/ja/analyzer.json b/locales/ja/analyzer.json
index a8ab48f47..ed6fd3491 100644
--- a/locales/ja/analyzer.json
+++ b/locales/ja/analyzer.json
@@ -211,5 +211,6 @@
"comp.removeAll": "",
"comp.addAll": "",
"comp.singleWeaponCombos": "",
- "comp.graphicSubtitle": ""
+ "comp.graphicSubtitle": "",
+ "comp.exportHint": ""
}
diff --git a/locales/ja/art.json b/locales/ja/art.json
index 707c457b4..d28910974 100644
--- a/locales/ja/art.json
+++ b/locales/ja/art.json
@@ -21,5 +21,6 @@
"delete.title": "本当にこの作品を削除しますか?",
"unlink.title": "本当にこの作品をプロフィールから削除しますか?{{username}}しか再び追加することはできません。",
"noArtForTag": "#{{tag}}の作品が見つかりませんでした。",
- "imageFailed": ""
+ "imageFailed": "",
+ "openImage": ""
}
diff --git a/locales/ko/analyzer.json b/locales/ko/analyzer.json
index 6ccc2aa71..5b2523043 100644
--- a/locales/ko/analyzer.json
+++ b/locales/ko/analyzer.json
@@ -211,5 +211,6 @@
"comp.removeAll": "",
"comp.addAll": "",
"comp.singleWeaponCombos": "",
- "comp.graphicSubtitle": ""
+ "comp.graphicSubtitle": "",
+ "comp.exportHint": ""
}
diff --git a/locales/ko/art.json b/locales/ko/art.json
index 654f7c885..fcaf0c8c3 100644
--- a/locales/ko/art.json
+++ b/locales/ko/art.json
@@ -21,5 +21,6 @@
"delete.title": "",
"unlink.title": "",
"noArtForTag": "",
- "imageFailed": ""
+ "imageFailed": "",
+ "openImage": ""
}
diff --git a/locales/nl/analyzer.json b/locales/nl/analyzer.json
index 559986a49..bbe72b670 100644
--- a/locales/nl/analyzer.json
+++ b/locales/nl/analyzer.json
@@ -219,5 +219,6 @@
"comp.removeAll": "",
"comp.addAll": "",
"comp.singleWeaponCombos": "",
- "comp.graphicSubtitle": ""
+ "comp.graphicSubtitle": "",
+ "comp.exportHint": ""
}
diff --git a/locales/nl/art.json b/locales/nl/art.json
index 852565d8a..4d3d4b4d7 100644
--- a/locales/nl/art.json
+++ b/locales/nl/art.json
@@ -23,5 +23,6 @@
"delete.title": "",
"unlink.title": "",
"noArtForTag": "",
- "imageFailed": ""
+ "imageFailed": "",
+ "openImage": ""
}
diff --git a/locales/pl/analyzer.json b/locales/pl/analyzer.json
index b9adfd20f..631a26eb4 100644
--- a/locales/pl/analyzer.json
+++ b/locales/pl/analyzer.json
@@ -227,5 +227,6 @@
"comp.removeAll": "",
"comp.addAll": "",
"comp.singleWeaponCombos": "",
- "comp.graphicSubtitle": ""
+ "comp.graphicSubtitle": "",
+ "comp.exportHint": ""
}
diff --git a/locales/pl/art.json b/locales/pl/art.json
index f801affdc..9929c2269 100644
--- a/locales/pl/art.json
+++ b/locales/pl/art.json
@@ -25,5 +25,6 @@
"delete.title": "",
"unlink.title": "",
"noArtForTag": "",
- "imageFailed": ""
+ "imageFailed": "",
+ "openImage": ""
}
diff --git a/locales/pt-BR/analyzer.json b/locales/pt-BR/analyzer.json
index fb72dce13..f5d7a26de 100644
--- a/locales/pt-BR/analyzer.json
+++ b/locales/pt-BR/analyzer.json
@@ -223,5 +223,6 @@
"comp.removeAll": "",
"comp.addAll": "",
"comp.singleWeaponCombos": "",
- "comp.graphicSubtitle": ""
+ "comp.graphicSubtitle": "",
+ "comp.exportHint": ""
}
diff --git a/locales/pt-BR/art.json b/locales/pt-BR/art.json
index 303786be2..4fd889008 100644
--- a/locales/pt-BR/art.json
+++ b/locales/pt-BR/art.json
@@ -24,5 +24,6 @@
"delete.title": "",
"unlink.title": "",
"noArtForTag": "",
- "imageFailed": ""
+ "imageFailed": "",
+ "openImage": ""
}
diff --git a/locales/ru/analyzer.json b/locales/ru/analyzer.json
index ebf3a53e4..b4e534cbf 100644
--- a/locales/ru/analyzer.json
+++ b/locales/ru/analyzer.json
@@ -227,5 +227,6 @@
"comp.removeAll": "",
"comp.addAll": "",
"comp.singleWeaponCombos": "",
- "comp.graphicSubtitle": ""
+ "comp.graphicSubtitle": "",
+ "comp.exportHint": ""
}
diff --git a/locales/ru/art.json b/locales/ru/art.json
index 08a0d70a6..a78245334 100644
--- a/locales/ru/art.json
+++ b/locales/ru/art.json
@@ -25,5 +25,6 @@
"delete.title": "Вы точно хотите удалить этот арт?",
"unlink.title": "Вы точно хотите удалить этот арт с вашего профиля (только {{username}} может добавить его обратно)?",
"noArtForTag": "",
- "imageFailed": ""
+ "imageFailed": "",
+ "openImage": ""
}
diff --git a/locales/zh/analyzer.json b/locales/zh/analyzer.json
index a29764961..91ef92042 100644
--- a/locales/zh/analyzer.json
+++ b/locales/zh/analyzer.json
@@ -214,5 +214,6 @@
"comp.removeAll": "移除所有",
"comp.addAll": "添加所有",
"comp.singleWeaponCombos": "",
- "comp.graphicSubtitle": ""
+ "comp.graphicSubtitle": "",
+ "comp.exportHint": ""
}
diff --git a/locales/zh/art.json b/locales/zh/art.json
index af36e7d61..fb1cf8c78 100644
--- a/locales/zh/art.json
+++ b/locales/zh/art.json
@@ -22,5 +22,6 @@
"delete.title": "您确定要删除这件作品吗?",
"unlink.title": "您确定要将这件作品从个人资料中移除吗(只有 {{username}} 可以重新添加)?",
"noArtForTag": "未能找到包含 #{{tag}} 的作品。",
- "imageFailed": ""
+ "imageFailed": "",
+ "openImage": ""
}