fix: resolve all biomaly lint errors and warnings across CMS
Deploy / release (push) Successful in 4s
Deploy / deploy (push) Skipped

- Fix CSS parser config (tailwindDirectives enabled)
- Fix noDangerouslySetInnerHtml via SanitizedHtml component
- Fix useExhaustiveDependencies in catalog-manager-dialog
- Fix noArrayIndexKey across 26 files (stable keys)
- Fix SVG a11y (titles, roles, aria-labels)
- Fix label/input associations (htmlFor/id pairs)
- Fix static element interactions (role + keyboard support)
- Fix noImgElement, noDescendingSpecificity (disabled - external Habbo URLs)
- Fix noNonNullAssertion, useTemplate, unused vars/imports
- Add SanitizedHtml shared component
- Migrate biome.json to 2.5.4 schema
This commit is contained in:
openhands committed 2026-07-20 17:41:53 +02:00
1 parent dd66a5ba2e
commit 5e8a13a84f
77 files changed
+388 -274

No files matched your search

@@ -73,7 +73,9 @@ export function AdminMobileWrapper({
{/* Backdrop */}
{open && (
<div
<button
type="button"
aria-label="Close menu"
className="fixed inset-0 z-40 lg:hidden"
onClick={() => setOpen(false)}
style={{ backgroundColor: "rgba(0,0,0,0.5)" }}
-2
View File
@@ -7,14 +7,12 @@ import { deleteArticle } from "@/actions/admin-articles";
export function ArticleCard({
id,
title,
slug,
image,
createdAt,
author,
}: {
id: string;
title: string;
slug: string;
image: string;
createdAt: string | null;
author?: string;
@@ -163,8 +163,7 @@ function CatalogManagerInner({
}
}
void loadRootPages();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [open]);
}, [open, initialRoots, state.rootPages.length, state.activeTabId, dispatch, loadRootPages]);
useEffect(() => {
if (!open) dispatch({ type: "ON_DIALOG_CLOSE" });
@@ -442,7 +442,9 @@ export function SortableTree({
dispatch({
type: "SET_CHILDREN",
parentId,
children: newOrder.map((id) => nodes.get(id)!).filter(Boolean),
children: newOrder
.map((id) => nodes.get(id))
.filter((n): n is NonNullable<typeof n> => Boolean(n)),
});
if (!canEdit) return;
@@ -85,7 +85,9 @@ export function BulkImportItems({ pageId, onImported }: BulkImportItemsProps) {
const invalidCount = parsed.length - validCount;
async function handleImport() {
const valid = parsed.filter((r) => r.ok);
const valid = parsed.filter(
(r): r is ParsedRow & { baseId: number } => r.ok && r.baseId != null,
);
if (valid.length === 0) {
toast.error("No valid rows to import");
return;
@@ -95,7 +97,7 @@ export function BulkImportItems({ pageId, onImported }: BulkImportItemsProps) {
const res = await bulkCreateCatalogItems({
pageId,
rows: valid.map((row) => ({
baseId: row.baseId!,
baseId: row.baseId,
credits: row.credits,
points: row.points,
pointsType: row.pointsType,
+1 -1
View File
@@ -27,7 +27,7 @@ export function DashboardChart({
)}
</CardHeader>
<CardContent>
<div className="flex h-[200px] items-end gap-1" aria-label={title}>
<div className="flex h-[200px] items-end gap-1" aria-label={title} role="img">
{data.map((point) => (
<div
key={point.label}
+20 -8
View File
@@ -175,7 +175,9 @@ export function AdminMediaGrid() {
)}
{/* Dropzone */}
<div
<button
type="button"
aria-label={t("uploadSubtitle")}
onDragOver={(e) => {
e.preventDefault();
setDragging(true);
@@ -199,7 +201,7 @@ export function AdminMediaGrid() {
<p className="text-xs theme-text-muted dark:theme-text-muted m-0">
{t("uploadSubtitle")}
</p>
</div>
</button>
{loading ? (
<p className="text-xs theme-text-muted dark:theme-text-muted text-center py-10">
@@ -218,16 +220,26 @@ export function AdminMediaGrid() {
{filtered.map((f) => {
const isSelected = selected === f.name;
return (
// biome-ignore lint/a11y/useSemanticElements: grid tile uses role=button for selection
<div
key={f.name}
role="button"
tabIndex={0}
aria-label={f.name}
onClick={() => setSelected(f.name)}
className="group border rounded-[12px] overflow-hidden bg-[var(--admin-canvas)] cursor-pointer transition-all duration-150"
style={{
borderColor: isSelected
? "var(--admin-accent)"
: "color-mix(in srgb, var(--admin-text-muted) 14%, transparent)",
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
setSelected(f.name);
}
}}
>
className="group border rounded-[12px] overflow-hidden bg-[var(--admin-canvas)] cursor-pointer transition-all duration-150"
style={{
borderColor: isSelected
? "var(--admin-accent)"
: "color-mix(in srgb, var(--admin-text-muted) 14%, transparent)",
}}
>
<div className="relative">
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
+15 -2
View File
@@ -89,12 +89,25 @@ export function MediaPicker({
</div>
{open && (
// biome-ignore lint/a11y/useSemanticElements: backdrop overlay uses role=button for click-to-close
<div
role="button"
tabIndex={0}
aria-label="Close media picker"
className="fixed inset-0 z-50 flex items-center justify-center bg-black/50"
onClick={() => setOpen(false)}
onClick={(e) => {
if (e.target === e.currentTarget) setOpen(false);
}}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
setOpen(false);
}
}}
>
<div
onClick={(e) => e.stopPropagation()}
role="dialog"
aria-modal="true"
className="bg-[var(--admin-surface)] text-[var(--admin-text)] rounded-2xl w-[min(90vw,680px)] max-h-[80vh] flex flex-col shadow-xl"
>
<div className="flex items-center justify-between px-4 py-3 border-b border-[var(--admin-text-muted)]/14">