mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-09-24 04:07:14 -05:00
Art & tier list maker tweaks
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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 = (
|
||||
<img
|
||||
alt=""
|
||||
src={previewUrl(art.url)}
|
||||
loading="lazy"
|
||||
onClick={onClick}
|
||||
onPointerEnter={enablePreview ? () => preloadImage(art.url) : undefined}
|
||||
ref={imageRef}
|
||||
className={enablePreview ? styles.thumbnail : undefined}
|
||||
data-testid="art-image"
|
||||
/>
|
||||
);
|
||||
|
||||
const img = onClick ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
onPointerEnter={() => preloadImage(art.url)}
|
||||
className={styles.thumbnailButton}
|
||||
aria-label={art.description || t("art:openImage")}
|
||||
>
|
||||
{image}
|
||||
</button>
|
||||
) : (
|
||||
image
|
||||
);
|
||||
|
||||
if (!art.author && canEdit) {
|
||||
return (
|
||||
<div>
|
||||
|
||||
@@ -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 ? (
|
||||
<div className="stack horizontal justify-end">
|
||||
<div className="stack horizontal justify-end">
|
||||
{selectedWeaponIds.length >= MAX_WEAPONS ? (
|
||||
<CompExportDialog
|
||||
weaponIds={selectedWeaponIds}
|
||||
excludedDamageKeys={excludedDamageKeys}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
) : (
|
||||
<SendouPopover trigger={<ExportButton />}>
|
||||
{t("analyzer:comp.exportHint", { max: MAX_WEAPONS })}
|
||||
</SendouPopover>
|
||||
)}
|
||||
</div>
|
||||
<WeaponCategories selectedWeaponIds={selectedWeaponIds} />
|
||||
<WeaponGrid
|
||||
selectedWeaponIds={selectedWeaponIds}
|
||||
@@ -139,15 +148,7 @@ function CompExportDialog({ weaponIds, excludedDamageKeys }: CompExportProps) {
|
||||
|
||||
return (
|
||||
<ImageExportDialog
|
||||
trigger={
|
||||
<SendouButton
|
||||
size="small"
|
||||
variant="outlined"
|
||||
icon={<HardDriveDownload />}
|
||||
>
|
||||
{t("common:imageExport.export")}
|
||||
</SendouButton>
|
||||
}
|
||||
trigger={<ExportButton />}
|
||||
heading={t("common:imageExport.export")}
|
||||
filename="comp"
|
||||
settings={
|
||||
@@ -212,3 +213,18 @@ function CompGraphicWithCombos({
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function ExportButton(props: SendouButtonProps) {
|
||||
const { t } = useTranslation(["common"]);
|
||||
|
||||
return (
|
||||
<SendouButton
|
||||
{...props}
|
||||
size="small"
|
||||
variant="outlined"
|
||||
icon={<HardDriveDownload />}
|
||||
>
|
||||
{t("common:imageExport.export")}
|
||||
</SendouButton>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -219,5 +219,6 @@
|
||||
"comp.removeAll": "",
|
||||
"comp.addAll": "",
|
||||
"comp.singleWeaponCombos": "",
|
||||
"comp.graphicSubtitle": ""
|
||||
"comp.graphicSubtitle": "",
|
||||
"comp.exportHint": ""
|
||||
}
|
||||
|
||||
@@ -23,5 +23,6 @@
|
||||
"delete.title": "",
|
||||
"unlink.title": "",
|
||||
"noArtForTag": "",
|
||||
"imageFailed": ""
|
||||
"imageFailed": "",
|
||||
"openImage": ""
|
||||
}
|
||||
|
||||
@@ -219,5 +219,6 @@
|
||||
"comp.removeAll": "",
|
||||
"comp.addAll": "",
|
||||
"comp.singleWeaponCombos": "",
|
||||
"comp.graphicSubtitle": ""
|
||||
"comp.graphicSubtitle": "",
|
||||
"comp.exportHint": ""
|
||||
}
|
||||
|
||||
@@ -23,5 +23,6 @@
|
||||
"delete.title": "",
|
||||
"unlink.title": "",
|
||||
"noArtForTag": "",
|
||||
"imageFailed": ""
|
||||
"imageFailed": "",
|
||||
"openImage": ""
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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": ""
|
||||
}
|
||||
|
||||
@@ -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": ""
|
||||
}
|
||||
|
||||
@@ -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": ""
|
||||
}
|
||||
|
||||
@@ -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": ""
|
||||
}
|
||||
|
||||
@@ -223,5 +223,6 @@
|
||||
"comp.removeAll": "",
|
||||
"comp.addAll": "",
|
||||
"comp.singleWeaponCombos": "",
|
||||
"comp.graphicSubtitle": ""
|
||||
"comp.graphicSubtitle": "",
|
||||
"comp.exportHint": ""
|
||||
}
|
||||
|
||||
@@ -24,5 +24,6 @@
|
||||
"delete.title": "",
|
||||
"unlink.title": "",
|
||||
"noArtForTag": "",
|
||||
"imageFailed": ""
|
||||
"imageFailed": "",
|
||||
"openImage": ""
|
||||
}
|
||||
|
||||
@@ -223,5 +223,6 @@
|
||||
"comp.removeAll": "",
|
||||
"comp.addAll": "",
|
||||
"comp.singleWeaponCombos": "",
|
||||
"comp.graphicSubtitle": ""
|
||||
"comp.graphicSubtitle": "",
|
||||
"comp.exportHint": ""
|
||||
}
|
||||
|
||||
@@ -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": ""
|
||||
}
|
||||
|
||||
@@ -223,5 +223,6 @@
|
||||
"comp.removeAll": "הסר הכל",
|
||||
"comp.addAll": "הוסף הכל",
|
||||
"comp.singleWeaponCombos": "",
|
||||
"comp.graphicSubtitle": ""
|
||||
"comp.graphicSubtitle": "",
|
||||
"comp.exportHint": ""
|
||||
}
|
||||
|
||||
@@ -24,5 +24,6 @@
|
||||
"delete.title": "האם אתה בטוח שאתה רוצה למחוק את הציור?",
|
||||
"unlink.title": "האם אתה בטוח שאתה רוצה להסיר את הציור הזה מהפרופיל שלך (רק {{username}} יוכל להוסיף אותו חזרה)?",
|
||||
"noArtForTag": "",
|
||||
"imageFailed": ""
|
||||
"imageFailed": "",
|
||||
"openImage": ""
|
||||
}
|
||||
|
||||
@@ -223,5 +223,6 @@
|
||||
"comp.removeAll": "",
|
||||
"comp.addAll": "",
|
||||
"comp.singleWeaponCombos": "",
|
||||
"comp.graphicSubtitle": ""
|
||||
"comp.graphicSubtitle": "",
|
||||
"comp.exportHint": ""
|
||||
}
|
||||
|
||||
@@ -24,5 +24,6 @@
|
||||
"delete.title": "",
|
||||
"unlink.title": "",
|
||||
"noArtForTag": "",
|
||||
"imageFailed": ""
|
||||
"imageFailed": "",
|
||||
"openImage": ""
|
||||
}
|
||||
|
||||
@@ -211,5 +211,6 @@
|
||||
"comp.removeAll": "",
|
||||
"comp.addAll": "",
|
||||
"comp.singleWeaponCombos": "",
|
||||
"comp.graphicSubtitle": ""
|
||||
"comp.graphicSubtitle": "",
|
||||
"comp.exportHint": ""
|
||||
}
|
||||
|
||||
@@ -21,5 +21,6 @@
|
||||
"delete.title": "本当にこの作品を削除しますか?",
|
||||
"unlink.title": "本当にこの作品をプロフィールから削除しますか?{{username}}しか再び追加することはできません。",
|
||||
"noArtForTag": "#{{tag}}の作品が見つかりませんでした。",
|
||||
"imageFailed": ""
|
||||
"imageFailed": "",
|
||||
"openImage": ""
|
||||
}
|
||||
|
||||
@@ -211,5 +211,6 @@
|
||||
"comp.removeAll": "",
|
||||
"comp.addAll": "",
|
||||
"comp.singleWeaponCombos": "",
|
||||
"comp.graphicSubtitle": ""
|
||||
"comp.graphicSubtitle": "",
|
||||
"comp.exportHint": ""
|
||||
}
|
||||
|
||||
@@ -21,5 +21,6 @@
|
||||
"delete.title": "",
|
||||
"unlink.title": "",
|
||||
"noArtForTag": "",
|
||||
"imageFailed": ""
|
||||
"imageFailed": "",
|
||||
"openImage": ""
|
||||
}
|
||||
|
||||
@@ -219,5 +219,6 @@
|
||||
"comp.removeAll": "",
|
||||
"comp.addAll": "",
|
||||
"comp.singleWeaponCombos": "",
|
||||
"comp.graphicSubtitle": ""
|
||||
"comp.graphicSubtitle": "",
|
||||
"comp.exportHint": ""
|
||||
}
|
||||
|
||||
@@ -23,5 +23,6 @@
|
||||
"delete.title": "",
|
||||
"unlink.title": "",
|
||||
"noArtForTag": "",
|
||||
"imageFailed": ""
|
||||
"imageFailed": "",
|
||||
"openImage": ""
|
||||
}
|
||||
|
||||
@@ -227,5 +227,6 @@
|
||||
"comp.removeAll": "",
|
||||
"comp.addAll": "",
|
||||
"comp.singleWeaponCombos": "",
|
||||
"comp.graphicSubtitle": ""
|
||||
"comp.graphicSubtitle": "",
|
||||
"comp.exportHint": ""
|
||||
}
|
||||
|
||||
@@ -25,5 +25,6 @@
|
||||
"delete.title": "",
|
||||
"unlink.title": "",
|
||||
"noArtForTag": "",
|
||||
"imageFailed": ""
|
||||
"imageFailed": "",
|
||||
"openImage": ""
|
||||
}
|
||||
|
||||
@@ -223,5 +223,6 @@
|
||||
"comp.removeAll": "",
|
||||
"comp.addAll": "",
|
||||
"comp.singleWeaponCombos": "",
|
||||
"comp.graphicSubtitle": ""
|
||||
"comp.graphicSubtitle": "",
|
||||
"comp.exportHint": ""
|
||||
}
|
||||
|
||||
@@ -24,5 +24,6 @@
|
||||
"delete.title": "",
|
||||
"unlink.title": "",
|
||||
"noArtForTag": "",
|
||||
"imageFailed": ""
|
||||
"imageFailed": "",
|
||||
"openImage": ""
|
||||
}
|
||||
|
||||
@@ -227,5 +227,6 @@
|
||||
"comp.removeAll": "",
|
||||
"comp.addAll": "",
|
||||
"comp.singleWeaponCombos": "",
|
||||
"comp.graphicSubtitle": ""
|
||||
"comp.graphicSubtitle": "",
|
||||
"comp.exportHint": ""
|
||||
}
|
||||
|
||||
@@ -25,5 +25,6 @@
|
||||
"delete.title": "Вы точно хотите удалить этот арт?",
|
||||
"unlink.title": "Вы точно хотите удалить этот арт с вашего профиля (только {{username}} может добавить его обратно)?",
|
||||
"noArtForTag": "",
|
||||
"imageFailed": ""
|
||||
"imageFailed": "",
|
||||
"openImage": ""
|
||||
}
|
||||
|
||||
@@ -214,5 +214,6 @@
|
||||
"comp.removeAll": "移除所有",
|
||||
"comp.addAll": "添加所有",
|
||||
"comp.singleWeaponCombos": "",
|
||||
"comp.graphicSubtitle": ""
|
||||
"comp.graphicSubtitle": "",
|
||||
"comp.exportHint": ""
|
||||
}
|
||||
|
||||
@@ -22,5 +22,6 @@
|
||||
"delete.title": "您确定要删除这件作品吗?",
|
||||
"unlink.title": "您确定要将这件作品从个人资料中移除吗(只有 {{username}} 可以重新添加)?",
|
||||
"noArtForTag": "未能找到包含 #{{tag}} 的作品。",
|
||||
"imageFailed": ""
|
||||
"imageFailed": "",
|
||||
"openImage": ""
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user