diff --git a/ApiCore-Frontend/scripts/codemod-t5-focus.mjs b/ApiCore-Frontend/scripts/codemod-t5-focus.mjs new file mode 100644 index 0000000..86747bb --- /dev/null +++ b/ApiCore-Frontend/scripts/codemod-t5-focus.mjs @@ -0,0 +1,132 @@ +#!/usr/bin/env node +/** + * T5 focus and size codemod (spec 4.5, 4.6 and 10.8 step 5). Pure and idempotent: + * + * - One focus style: the base rule `:where(:focus-visible)` in globals.css draws it. Component + * classes that remove the outline or draw an own ring go: `outline-none` (also behind + * `focus:`/`focus-visible:`), `focus-visible:ring-3`, `focus-visible:ring-ring/50`, + * `focus-visible:border-ring`, own outline rings (`focus-visible:outline-2 …-ring`, the hive + * glass edge ring, which `.holo-stage` now provides) and variant focus colours. + * - A row whose focus target sits inside (label with its input) keeps one ring on the row: + * `has-[:focus-visible]:outline-*`/`ring-*` become the utility `focus-ring-within`. + * - Compact sizes follow the pointer, not the viewport: `sm:h-*`/`sm:min-h-*`/`sm:size-*` (also + * behind a further variant) become `pointer-fine:…`. + * - `focus:outline-2 focus:outline-ring` stays: it marks elements focused by script after a + * click, where :focus-visible does not match (guarded with an allow-list). + * - A class string that held nothing but focus classes is dropped together with its line. + * + * Selection outlines (`outline-2 outline-foreground`) and menu items' `outline-hidden` stay. + * + * CLI: node scripts/codemod-t5-focus.mjs [--check] + * --check lists the files that would change and exits 1 if there are any. + */ +import { readdirSync, readFileSync, statSync, writeFileSync } from "node:fs"; +import { join, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +/** Characters that belong to a class token (so a match never starts or ends inside one). */ +const TOKEN_CHAR = String.raw`[\w:/\[\]().%-]`; + +const escape = (text) => text.replace(/[.*+?^${}()|[\]\\/]/g, "\\$&"); + +/** Classes removed outright. */ +const REMOVED = [ + "outline-none", + "focus:outline-none", + "focus-visible:outline-none", + "focus-visible:ring-3", + "focus-visible:ring-[3px]", + "focus-visible:ring-ring/50", + "focus-visible:border-ring", + "focus:ring-3", + "focus:ring-ring/50", + "focus-visible:outline-1", + "focus-visible:outline-2", + "focus-visible:outline-ring", + "focus-visible:outline-offset-2", + "focus-visible:outline-holo-edge-strong", + "focus-visible:ring-destructive/20", + "focus-visible:border-destructive/40", + "focus-visible:ring-warning/30", +]; + +/** Own rings on a row with an inner focus target; replaced by one `focus-ring-within`. */ +const WITHIN = [ + "has-[:focus-visible]:outline-2", + "has-[:focus-visible]:outline-ring", + "has-[:focus-visible]:ring-3", + "has-[:focus-visible]:ring-ring/50", + "has-focus-visible:ring-3", + "has-focus-visible:ring-ring/50", +]; +const WITHIN_UTILITY = "focus-ring-within"; + +function alternativesOf(classes) { + return `(?:${classes.map(escape).join("|")})(?!${TOKEN_CHAR})`; +} + +/** + * Removes the classes so that exactly one separator stays: a class after a space goes with + * that space, a class at the start of a string goes with the space after it. + */ +function removeClasses(line, classes) { + const alternatives = alternativesOf(classes); + return line + .replace(new RegExp(` ${alternatives}`, "g"), "") + .replace(new RegExp(`(? sourceFiles(join(path, name))); + } + return /\.tsx?$/.test(path) && !/\.spec\./.test(path) ? [path] : []; +} + +function main(args) { + const check = args.includes("--check"); + const targets = args.filter((arg) => arg !== "--check"); + if (targets.length === 0) { + console.error("usage: node scripts/codemod-t5-focus.mjs [--check] "); + return 2; + } + const changed = targets.flatMap(sourceFiles).filter((file) => { + const before = readFileSync(file, "utf8"); + const after = transform(before); + if (after === before) return false; + if (!check) writeFileSync(file, after); + return true; + }); + for (const file of changed) console.log(file); + console.log(`${changed.length} file(s) ${check ? "to migrate" : "migrated"}`); + return check && changed.length > 0 ? 1 : 0; +} + +if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) { + process.exitCode = main(process.argv.slice(2)); +} diff --git a/ApiCore-Frontend/scripts/codemod-t5-focus.spec.mjs b/ApiCore-Frontend/scripts/codemod-t5-focus.spec.mjs new file mode 100644 index 0000000..3349a00 --- /dev/null +++ b/ApiCore-Frontend/scripts/codemod-t5-focus.spec.mjs @@ -0,0 +1,121 @@ +import { readdirSync, readFileSync, statSync } from "node:fs"; +import { join } from "node:path"; +import { describe, expect, it } from "vitest"; +import { transform } from "./codemod-t5-focus.mjs"; + +describe("codemod t5 focus and sizes (spec 4.5, 4.6, 10.8 step 5)", () => { + it("drops outline-none and the component focus ring classes", () => { + expect( + transform( + '"min-h-11 rounded-lg px-2 outline-none hover:bg-muted focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:opacity-60"', + ), + ).toBe('"min-h-11 rounded-lg px-2 hover:bg-muted disabled:opacity-60"'); + expect(transform('className="px-2 focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-none"')).toBe( + 'className="px-2"', + ); + }); + + it("drops the own outline rings, including the stage edge ring of the hive glass", () => { + expect(transform('"rounded-full focus-visible:outline-2 focus-visible:outline-ring"')).toBe('"rounded-full"'); + expect( + transform("`h-11 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-holo-edge-strong ${X}`"), + ).toBe("`h-11 ${X}`"); + }); + + it("keeps the focus: outline of elements focused by script after a click", () => { + // :focus-visible does not match a script focus that follows a pointer click. + const scripted = '"rounded-xl border p-3 focus:outline-2 focus:outline-ring"'; + expect(transform(scripted)).toBe(scripted); + }); + + it("drops the skip link's own focus ring", () => { + expect(transform('"sr-only focus:not-sr-only focus:ring-3 focus:ring-ring/50 focus:outline-none"')).toBe( + '"sr-only focus:not-sr-only"', + ); + }); + + it("drops variant focus colours on the destructive button and badges", () => { + expect(transform('"bg-destructive/10 text-destructive focus-visible:border-destructive/40 focus-visible:ring-destructive/20"')).toBe( + '"bg-destructive/10 text-destructive"', + ); + expect(transform('"bg-warning/15 focus-visible:ring-warning/30 [a]:hover:bg-warning/25"')).toBe( + '"bg-warning/15 [a]:hover:bg-warning/25"', + ); + }); + + it("a row with an inner focus target keeps one ring on the outer element", () => { + expect(transform('"flex gap-3 rounded-xl border p-3 has-[:focus-visible]:outline-2 has-[:focus-visible]:outline-ring"')).toBe( + '"flex gap-3 rounded-xl border p-3 focus-ring-within"', + ); + expect(transform('"flex min-h-14 px-3 has-[:focus-visible]:ring-3 has-[:focus-visible]:ring-ring/50"')).toBe( + '"flex min-h-14 px-3 focus-ring-within"', + ); + expect(transform('"flex min-h-11 px-1 has-focus-visible:ring-3 has-focus-visible:ring-ring/50"')).toBe( + '"flex min-h-11 px-1 focus-ring-within"', + ); + }); + + it("selection outlines that are not focus styles stay", () => { + const calendar = 'isSelected && "outline-2 -outline-offset-2 outline-foreground",'; + expect(transform(calendar)).toBe(calendar); + const canvas = 'selected && "outline outline-2 outline-foreground",'; + expect(transform(canvas)).toBe(canvas); + const menuItem = '"relative flex rounded-md text-sm outline-hidden select-none focus:bg-accent"'; + expect(transform(menuItem)).toBe(menuItem); + }); + + it("a class string that only held focus classes disappears with its line", () => { + expect(transform('cn(\n "min-h-11 px-2",\n "focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50",\n x,\n)')).toBe( + 'cn(\n "min-h-11 px-2",\n x,\n)', + ); + }); + + it("sm:h-* and sm:min-h-* become pointer-fine, also behind a further variant", () => { + expect(transform('"h-10 sm:h-8 gap-1.5"')).toBe('"h-10 pointer-fine:h-8 gap-1.5"'); + expect(transform('"min-h-11 sm:min-h-9 rounded-lg"')).toBe('"min-h-11 pointer-fine:min-h-9 rounded-lg"'); + expect(transform('"data-[size=default]:h-11 sm:data-[size=default]:h-9"')).toBe( + '"data-[size=default]:h-11 pointer-fine:data-[size=default]:h-9"', + ); + }); + + it("sm:size-* becomes pointer-fine:size-*", () => { + expect(transform('className="size-11 sm:size-8"')).toBe('className="size-11 pointer-fine:size-8"'); + }); + + it("other sm: classes stay", () => { + const layout = '"hidden sm:flex sm:hidden sm:gap-2 sm:px-4 sm:max-w-xs sm:max-h-96"'; + expect(transform(layout)).toBe(layout); + }); + + it("keeps CRLF line endings", () => { + expect(transform('a\r\nclassName="px-2 outline-none"\r\n')).toBe('a\r\nclassName="px-2"\r\n'); + }); + + it("the focus codemod is idempotent and keeps pointer-fine classes", () => { + const source = [ + '"h-11 sm:h-8 outline-none focus-visible:ring-3 focus-visible:ring-ring/50"', + '"pointer-fine:h-9 has-[:focus-visible]:outline-2 has-[:focus-visible]:outline-ring"', + ].join("\n"); + const once = transform(source); + expect(once).toBe('"h-11 pointer-fine:h-8"\n"pointer-fine:h-9 focus-ring-within"'); + expect(transform(once)).toBe(once); + }); + + it("leaves nothing to migrate in src", () => { + const files = sourceFiles(join(process.cwd(), "src")); + expect(files.length).toBeGreaterThan(100); + const pending = files.filter((file) => { + const text = readFileSync(file, "utf8"); + return transform(text) !== text; + }); + expect(pending).toEqual([]); + }); +}); + +function sourceFiles(dir) { + return readdirSync(dir).flatMap((name) => { + const path = join(dir, name); + if (statSync(path).isDirectory()) return sourceFiles(path); + return /\.tsx?$/.test(name) && !/\.spec\./.test(name) ? [path] : []; + }); +} diff --git a/ApiCore-Frontend/src/app/(app)/apiaries/[id]/apiary-colonies-card.tsx b/ApiCore-Frontend/src/app/(app)/apiaries/[id]/apiary-colonies-card.tsx index 0102552..efa4c24 100644 --- a/ApiCore-Frontend/src/app/(app)/apiaries/[id]/apiary-colonies-card.tsx +++ b/ApiCore-Frontend/src/app/(app)/apiaries/[id]/apiary-colonies-card.tsx @@ -78,7 +78,7 @@ export function ColonyOverviewLinks({ overview, hrefFor }: ColonyOverviewLinksPr diff --git a/ApiCore-Frontend/src/app/(app)/bottling/[id]/print-labels-card.tsx b/ApiCore-Frontend/src/app/(app)/bottling/[id]/print-labels-card.tsx index af88514..e6b3155 100644 --- a/ApiCore-Frontend/src/app/(app)/bottling/[id]/print-labels-card.tsx +++ b/ApiCore-Frontend/src/app/(app)/bottling/[id]/print-labels-card.tsx @@ -26,7 +26,7 @@ const PRINT_DONE = "Etiketten-PDF heruntergeladen."; const PROOF_DONE = "Probedruck (MUSTER) heruntergeladen. Er ist nicht zum Verkauf gedacht."; const CHECK_FALLBACK = "Die Pflichtangaben konnten nicht geprüft werden."; const LINK_CLASS = - "inline-flex min-h-11 items-center rounded-md text-sm font-medium text-primary-ink underline underline-offset-4 focus-visible:outline-2 focus-visible:outline-ring"; + "inline-flex min-h-11 items-center rounded-md text-sm font-medium text-primary-ink underline underline-offset-4"; type FixTarget = "master" | "product" | "designer"; diff --git a/ApiCore-Frontend/src/app/(app)/calendar/page.spec.tsx b/ApiCore-Frontend/src/app/(app)/calendar/page.spec.tsx index 15089cc..7411c25 100644 --- a/ApiCore-Frontend/src/app/(app)/calendar/page.spec.tsx +++ b/ApiCore-Frontend/src/app/(app)/calendar/page.spec.tsx @@ -192,6 +192,20 @@ describe("CalendarPage entries", () => { expect(link).toHaveTextContent("Wartezeit bis einschließlich 12.08. – ab 13.08. wieder erntefähig"); }); + it("marks the chosen day with an inset ring and keeps the focus ring inside the clipped grid", async () => { + renderPage(); + await screen.findAllByText("Wartezeit B3 · Oxuvar", { selector: "span.truncate" }); + const day = screen.getByRole("button", { name: /Dienstag, 11\. August/ }); + await userEvent.click(day); + + const classes = day.className.split(/\s+/); + expect(classes).toEqual(expect.arrayContaining(["ring-2", "ring-inset", "ring-foreground"])); + // A selection outline would replace the focus ring; the grid clips (overflow-hidden), so + // the focus ring of every day is drawn inside the cell, next to the selection ring. + expect(day.className).not.toMatch(/(^|\s)outline-(2|foreground)|-outline-offset-2/); + expect(classes).toContain("focus-visible:outline-offset-[calc(-2px_-_var(--focus-width))]"); + }); + it("counts a day with several entries in the plural", async () => { vi.mocked(fetchCalendarEvents).mockResolvedValue([ { id: "a", type: "TASK", date: "2026-08-12T00:00:00.000Z", title: "A", relatedType: "Task", relatedId: "1" }, diff --git a/ApiCore-Frontend/src/app/(app)/calendar/page.tsx b/ApiCore-Frontend/src/app/(app)/calendar/page.tsx index 095499f..eb17748 100644 --- a/ApiCore-Frontend/src/app/(app)/calendar/page.tsx +++ b/ApiCore-Frontend/src/app/(app)/calendar/page.tsx @@ -146,7 +146,7 @@ function EventRow({ event, isContinuation }: { event: CalendarEvent; isContinuat ); return href ? ( - + {content} ) : ( @@ -353,9 +353,11 @@ export default function CalendarPage() { aria-pressed={isSelected} aria-label={`${dayHeading(day)}${isToday ? ", heute" : ""}: ${entryCountText(dayEvents.length)}`} className={cn( - "flex min-h-24 flex-col gap-1 border-r border-b p-1.5 text-left last:border-r-0", + // The grid clips (overflow-hidden): the focus ring sits inside the cell, + // next to the inset selection ring, so edge days keep it whole. + "flex min-h-24 flex-col gap-1 border-r border-b p-1.5 text-left last:border-r-0 focus-visible:outline-offset-[calc(-2px_-_var(--focus-width))]", isCurrentMonth ? "bg-card" : "bg-muted/40", - isSelected && "outline-2 -outline-offset-2 outline-foreground", + isSelected && "ring-2 ring-inset ring-foreground", )} > {error}

diff --git a/ApiCore-Frontend/src/app/(app)/colonies/[id]/colony-edit-card.tsx b/ApiCore-Frontend/src/app/(app)/colonies/[id]/colony-edit-card.tsx index 2a0d67c..ce28205 100644 --- a/ApiCore-Frontend/src/app/(app)/colonies/[id]/colony-edit-card.tsx +++ b/ApiCore-Frontend/src/app/(app)/colonies/[id]/colony-edit-card.tsx @@ -283,7 +283,7 @@ export function ColonyEditCard({ colony }: { colony: Colony }) { ref={conflictRef} role="alert" tabIndex={-1} - className="text-sm font-medium text-destructive outline-none" + className="text-sm font-medium text-destructive" > {conflictText}

diff --git a/ApiCore-Frontend/src/app/(app)/colonies/[id]/hive/page.tsx b/ApiCore-Frontend/src/app/(app)/colonies/[id]/hive/page.tsx index 900879e..6611db0 100644 --- a/ApiCore-Frontend/src/app/(app)/colonies/[id]/hive/page.tsx +++ b/ApiCore-Frontend/src/app/(app)/colonies/[id]/hive/page.tsx @@ -66,7 +66,7 @@ export default function HiveEditorPage() { ); } if (!colony.data || !hive.data || !catalog) { - return
; + return
; } return ( {submitError}

diff --git a/ApiCore-Frontend/src/app/(app)/compliance/notices/[id]/notice-steps.tsx b/ApiCore-Frontend/src/app/(app)/compliance/notices/[id]/notice-steps.tsx index 5a65678..b00e3da 100644 --- a/ApiCore-Frontend/src/app/(app)/compliance/notices/[id]/notice-steps.tsx +++ b/ApiCore-Frontend/src/app/(app)/compliance/notices/[id]/notice-steps.tsx @@ -39,7 +39,7 @@ function Step({ number, title, titleId, state, children }: StepProps) { {`${number}. ${title}`} diff --git a/ApiCore-Frontend/src/app/(app)/compliance/notices/[id]/page.tsx b/ApiCore-Frontend/src/app/(app)/compliance/notices/[id]/page.tsx index 8c2e97b..d697559 100644 --- a/ApiCore-Frontend/src/app/(app)/compliance/notices/[id]/page.tsx +++ b/ApiCore-Frontend/src/app/(app)/compliance/notices/[id]/page.tsx @@ -164,7 +164,7 @@ export default function NoticeDetailPage() { id="notice-flow-heading" ref={flowHeading} tabIndex={-1} - className="font-heading text-lg font-semibold tracking-tight outline-none" + className="font-heading text-lg font-semibold tracking-tight" > {flowTitle} diff --git a/ApiCore-Frontend/src/app/(app)/compliance/notices/create-notice-dialog.tsx b/ApiCore-Frontend/src/app/(app)/compliance/notices/create-notice-dialog.tsx index eaae974..edef04e 100644 --- a/ApiCore-Frontend/src/app/(app)/compliance/notices/create-notice-dialog.tsx +++ b/ApiCore-Frontend/src/app/(app)/compliance/notices/create-notice-dialog.tsx @@ -197,7 +197,7 @@ function CreateNoticeForm({ onDone }: { onDone: () => void }) {

{showProblem ? problem : null}

-
+
{error ? (

{error.text}

diff --git a/ApiCore-Frontend/src/app/(app)/compliance/notices/page.tsx b/ApiCore-Frontend/src/app/(app)/compliance/notices/page.tsx index 5cb6eb9..6ceb2a6 100644 --- a/ApiCore-Frontend/src/app/(app)/compliance/notices/page.tsx +++ b/ApiCore-Frontend/src/app/(app)/compliance/notices/page.tsx @@ -62,7 +62,7 @@ export default function NoticesPage() { id="notice-kind-filter" value={kind} onChange={(event) => setKind(event.target.value as KindFilter)} - className="h-11 w-full rounded-xl border border-input bg-card px-3 text-base sm:max-w-xs md:text-sm" + className="h-11 w-full rounded-xl border border-input bg-card px-3 text-base sm:max-w-xs" > {NOTICE_KINDS.map((entry) => ( @@ -85,7 +85,7 @@ export default function NoticesPage() { aria-invalid={yearInvalid ? true : undefined} aria-describedby={yearInvalid ? "notice-year-problem" : undefined} onChange={(event) => setYearText(event.target.value)} - className="h-11 w-28 rounded-xl border border-input bg-card px-3 text-base md:text-sm" + className="h-11 w-28 rounded-xl border border-input bg-card px-3 text-base" /> {yearInvalid ? (

@@ -131,7 +131,7 @@ function NoticeLink({ notice }: { notice: AuthorityNotice }) { return ( {noticeTitle(notice)} diff --git a/ApiCore-Frontend/src/app/(app)/compliance/readiness-item-card.tsx b/ApiCore-Frontend/src/app/(app)/compliance/readiness-item-card.tsx index 7f5fcc6..e89f286 100644 --- a/ApiCore-Frontend/src/app/(app)/compliance/readiness-item-card.tsx +++ b/ApiCore-Frontend/src/app/(app)/compliance/readiness-item-card.tsx @@ -17,7 +17,7 @@ const STATE_ICON: Record = { }; const FIX_LINK_CLASS = - "inline-flex min-h-11 shrink-0 items-center gap-1 rounded-lg px-2 font-medium text-primary-ink underline-offset-4 outline-none hover:underline focus-visible:ring-3 focus-visible:ring-ring/50"; + "inline-flex min-h-11 shrink-0 items-center gap-1 rounded-lg px-2 font-medium text-primary-ink underline-offset-4 hover:underline"; function FixLink({ itemKey, subject, context }: { itemKey: string; subject: ReadinessSubject | null; context: string }) { const { href, label, available } = fixLinkFor(itemKey, subject); @@ -117,7 +117,7 @@ export function ReadinessItemCard({ item, canManage }: ReadinessItemCardProps) {

{item.title}

diff --git a/ApiCore-Frontend/src/app/(app)/compliance/restrictions/page.tsx b/ApiCore-Frontend/src/app/(app)/compliance/restrictions/page.tsx index d9528ef..5f3e652 100644 --- a/ApiCore-Frontend/src/app/(app)/compliance/restrictions/page.tsx +++ b/ApiCore-Frontend/src/app/(app)/compliance/restrictions/page.tsx @@ -46,7 +46,7 @@ function Section({ const headingId = `restrictions-${id}`; return (
-

+

{`${title} (${rows.length})`}

{rows.length === 0 ? ( diff --git a/ApiCore-Frontend/src/app/(app)/dashboard/page.tsx b/ApiCore-Frontend/src/app/(app)/dashboard/page.tsx index 5d06995..79abb77 100644 --- a/ApiCore-Frontend/src/app/(app)/dashboard/page.tsx +++ b/ApiCore-Frontend/src/app/(app)/dashboard/page.tsx @@ -244,7 +244,7 @@ export default function DashboardPage() {
  • {harvests?.map((harvest) => ( {apiaryColonies.map((colony) => { const withdrawal = withdrawalByColony.get(colony.id); diff --git a/ApiCore-Frontend/src/app/(app)/inspections/new/hive-section.tsx b/ApiCore-Frontend/src/app/(app)/inspections/new/hive-section.tsx index 0822773..49f3003 100644 --- a/ApiCore-Frontend/src/app/(app)/inspections/new/hive-section.tsx +++ b/ApiCore-Frontend/src/app/(app)/inspections/new/hive-section.tsx @@ -147,7 +147,7 @@ export function InspectionHiveSection({ // A portal form inside the card (frame/part sheet) must never submit the enclosing inspection form.
    event.stopPropagation()} >

    diff --git a/ApiCore-Frontend/src/app/(app)/layout.tsx b/ApiCore-Frontend/src/app/(app)/layout.tsx index d83afdf..9b90b8a 100644 --- a/ApiCore-Frontend/src/app/(app)/layout.tsx +++ b/ApiCore-Frontend/src/app/(app)/layout.tsx @@ -85,7 +85,7 @@ export default function AppLayout({ children }: { children: ReactNode }) { pageContentRef.current?.focus(); pageContentRef.current?.scrollIntoView?.({ block: "start" }); }} - className="sr-only focus:not-sr-only focus:fixed focus:top-2 focus:left-2 focus:z-50 focus:rounded-lg focus:bg-card focus:px-4 focus:py-3 focus:text-sm focus:font-medium focus:shadow-[var(--shadow-elevated)] focus:ring-3 focus:ring-ring/50 focus:outline-none" + className="sr-only focus:not-sr-only focus:fixed focus:top-2 focus:left-2 focus:z-50 focus:rounded-lg focus:bg-card focus:px-4 focus:py-3 focus:text-sm focus:font-medium focus:shadow-[var(--shadow-elevated)]" > Zum Inhalt springen @@ -148,7 +148,7 @@ export default function AppLayout({ children }: { children: ReactNode }) { id={MAIN_CONTENT_ID} ref={pageContentRef} tabIndex={-1} - className={`flex-1 ${contentPaddingClass} outline-none sm:pb-(--safe-area-bottom)`} + className={`flex-1 ${contentPaddingClass} sm:pb-(--safe-area-bottom)`} > {children}

    diff --git a/ApiCore-Frontend/src/app/(app)/medicines/[id]/lot-form-dialog.tsx b/ApiCore-Frontend/src/app/(app)/medicines/[id]/lot-form-dialog.tsx index 7a37bbe..ee47188 100644 --- a/ApiCore-Frontend/src/app/(app)/medicines/[id]/lot-form-dialog.tsx +++ b/ApiCore-Frontend/src/app/(app)/medicines/[id]/lot-form-dialog.tsx @@ -50,7 +50,7 @@ interface LotFormValues { type LotFormErrors = Partial>; -const INPUT_CLASS = "h-11 sm:h-11"; +const INPUT_CLASS = "h-11 pointer-fine:h-11"; function initialValues(medicine: LotMedicine, lot?: LotView): LotFormValues { if (lot) { diff --git a/ApiCore-Frontend/src/app/(app)/medicines/lots/[id]/disposal-dialog.tsx b/ApiCore-Frontend/src/app/(app)/medicines/lots/[id]/disposal-dialog.tsx index 5da0732..344c87e 100644 --- a/ApiCore-Frontend/src/app/(app)/medicines/lots/[id]/disposal-dialog.tsx +++ b/ApiCore-Frontend/src/app/(app)/medicines/lots/[id]/disposal-dialog.tsx @@ -129,7 +129,7 @@ function DisposalForm({ lotId, remaining, unit, onDone }: DisposalFormProps) { setQuantityText(event.target.value)} /> @@ -141,7 +141,7 @@ function DisposalForm({ lotId, remaining, unit, onDone }: DisposalFormProps) { setCounted(event.target.value)} diff --git a/ApiCore-Frontend/src/app/(app)/medicines/medicine-form-dialog.tsx b/ApiCore-Frontend/src/app/(app)/medicines/medicine-form-dialog.tsx index 8a69f5e..ef673b3 100644 --- a/ApiCore-Frontend/src/app/(app)/medicines/medicine-form-dialog.tsx +++ b/ApiCore-Frontend/src/app/(app)/medicines/medicine-form-dialog.tsx @@ -89,7 +89,7 @@ interface MedicineFormProps { onDone: () => void; } -const INPUT_CLASS = "h-11 sm:h-11"; +const INPUT_CLASS = "h-11 pointer-fine:h-11"; function MedicineForm({ medicine, prefill, onDone }: MedicineFormProps) { const queryClient = useQueryClient(); diff --git a/ApiCore-Frontend/src/app/(app)/offline/page.tsx b/ApiCore-Frontend/src/app/(app)/offline/page.tsx index 4a0a4dd..b40153a 100644 --- a/ApiCore-Frontend/src/app/(app)/offline/page.tsx +++ b/ApiCore-Frontend/src/app/(app)/offline/page.tsx @@ -72,7 +72,7 @@ export default function OfflinePage() { {stored.pack.apiary.name} diff --git a/ApiCore-Frontend/src/app/(app)/settings/automation/rule-card-shell.tsx b/ApiCore-Frontend/src/app/(app)/settings/automation/rule-card-shell.tsx index de1134b..fffbbc0 100644 --- a/ApiCore-Frontend/src/app/(app)/settings/automation/rule-card-shell.tsx +++ b/ApiCore-Frontend/src/app/(app)/settings/automation/rule-card-shell.tsx @@ -29,7 +29,7 @@ export function RuleCardShell({ rule, open, onToggle, children }: RuleCardShellP aria-expanded={open} aria-controls={contentId} onClick={onToggle} - className="flex min-h-11 w-full items-start gap-3 rounded-xl px-4 py-1 text-left focus-visible:ring-3 focus-visible:ring-ring/50" + className="flex min-h-11 w-full items-start gap-3 rounded-xl px-4 py-1 text-left" > diff --git a/ApiCore-Frontend/src/app/(app)/settings/label-templates/designer/element-properties-panel.tsx b/ApiCore-Frontend/src/app/(app)/settings/label-templates/designer/element-properties-panel.tsx index 184e009..e8636c7 100644 --- a/ApiCore-Frontend/src/app/(app)/settings/label-templates/designer/element-properties-panel.tsx +++ b/ApiCore-Frontend/src/app/(app)/settings/label-templates/designer/element-properties-panel.tsx @@ -44,7 +44,8 @@ export function ElementPropertiesPanel({

    {element.type === "text" ? "Text" : element.type === "qrCode" ? "QR-Code" : element.type === "image" ? "Bild" : "Linie"}

    -
    + {/* pointer-coarse:gap-4: the 44 px hit areas of the two 28 px buttons must not overlap. */} +
    @@ -202,7 +203,7 @@ function TextFields({ />
    -
    +
    -
  • @@ -155,7 +155,7 @@ function BackLink({ href, label }: { href: string; label: string }) { return ( {label} diff --git a/ApiCore-Frontend/src/components/empty-state.tsx b/ApiCore-Frontend/src/components/empty-state.tsx index 221c77d..7f348fb 100644 --- a/ApiCore-Frontend/src/components/empty-state.tsx +++ b/ApiCore-Frontend/src/components/empty-state.tsx @@ -17,7 +17,7 @@ export function EmptyState({ icon: Icon, title, description, action }: EmptyStat

    {title}

    - {description ?

    {description}

    : null} + {description ?

    {description}

    : null}
    {action ?
    {action}
    : null}
    diff --git a/ApiCore-Frontend/src/components/entity-card.tsx b/ApiCore-Frontend/src/components/entity-card.tsx index 9c272f3..b14fbe6 100644 --- a/ApiCore-Frontend/src/components/entity-card.tsx +++ b/ApiCore-Frontend/src/components/entity-card.tsx @@ -37,7 +37,7 @@ export function EntityCard({
    -

    {title}

    +

    {title}

    {subtitle ? (

    {subtitle}

    ) : null} diff --git a/ApiCore-Frontend/src/components/form/choice-group.spec.tsx b/ApiCore-Frontend/src/components/form/choice-group.spec.tsx index 4b6c46f..f5a458a 100644 --- a/ApiCore-Frontend/src/components/form/choice-group.spec.tsx +++ b/ApiCore-Frontend/src/components/form/choice-group.spec.tsx @@ -221,4 +221,12 @@ describe("choiceOptions", () => { { value: "NONE", label: "Keine" }, ]); }); + + it("options are body text (16 px) and take the global focus ring", () => { + render(); + for (const radio of screen.getAllByRole("radio")) { + expect(radio.className.split(/\s+/)).toContain("text-base"); + expect(radio.className).not.toMatch(/outline-none|ring-ring\/50|focus-visible:ring-|focus-visible:border-ring/); + } + }); }); diff --git a/ApiCore-Frontend/src/components/form/choice-group.tsx b/ApiCore-Frontend/src/components/form/choice-group.tsx index e6c999b..acbd755 100644 --- a/ApiCore-Frontend/src/components/form/choice-group.tsx +++ b/ApiCore-Frontend/src/components/form/choice-group.tsx @@ -135,8 +135,7 @@ export function ChoiceGroup({ } onKeyDown={(event) => handleKeyDown(event, index)} className={cn( - "min-h-11 rounded-lg border px-2 text-sm outline-none motion-safe:transition-colors", - "focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50", + "min-h-11 rounded-lg border px-2 text-base motion-safe:transition-colors", !selected && "border-border bg-background hover:bg-muted", option.disabled && "cursor-not-allowed opacity-50 hover:bg-background", selected && isUnanswered && "border-foreground/30 bg-muted font-medium text-foreground", diff --git a/ApiCore-Frontend/src/components/form/colony-multi-select.tsx b/ApiCore-Frontend/src/components/form/colony-multi-select.tsx index 6be3445..8fc78ba 100644 --- a/ApiCore-Frontend/src/components/form/colony-multi-select.tsx +++ b/ApiCore-Frontend/src/components/form/colony-multi-select.tsx @@ -147,7 +147,7 @@ export function ColonyMultiSelect({