From d8238855fc3fb950ea0fc441a39585a2174de7b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nick=20Rosenl=C3=B6cher?= Date: Fri, 16 Oct 2026 17:23:21 +0200 Subject: [PATCH] =?UTF-8?q?feat(t5):=20neue=20Farbwerte,=20Dunkel=20f?= =?UTF-8?q?=C3=BCr=20alle,=20Farb-Codemod=20(T5a=20A4)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - globals.css: Hell, Dunkel und Druck mit den Spec-Werten aus 4.2, Fokusring = --primary-ink, Verlaufs-Token entfernt. - Codemod scripts/codemod-t5-colors.mjs: text-primary/-warning auf -ink, Vordergrund nur auf voller Fläche, Auswahlrahmen dunkel; --check-Modus und Spec halten src sauber. - Kontrast-Wächter misst die echte globals.css (VALUES_SWITCHED), alle 21 Zeilen aus 4.4 in allen vier Sätzen; keine dark:-Klassen mehr in src. - Dunkel ist ohne Entwickler-Schalter wählbar, Standard bleibt Hell. - Review-Nacharbeit: Auswahl-Umrisse und aktueller Schritt in Vordergrund, deaktivierte Felder bg-muted, Avatar ohne Blend, Honig-Check-Rahmen über --input. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../scripts/codemod-t5-colors.mjs | 87 ++++++ .../scripts/codemod-t5-colors.spec.mjs | 98 +++++++ .../app/(app)/apiaries/[id]/weather-card.tsx | 6 +- .../(app)/bottling/[id]/print-labels-card.tsx | 4 +- .../src/app/(app)/bottling/new/page.tsx | 4 +- ApiCore-Frontend/src/app/(app)/bottom-nav.tsx | 2 +- .../src/app/(app)/calendar/page.tsx | 2 +- .../app/(app)/colonies/[id]/colony-varroa.tsx | 4 +- .../src/app/(app)/colonies/new/page.tsx | 1 - .../compliance/notices/[id]/notice-steps.tsx | 2 +- .../notices/create-notice-dialog.tsx | 2 +- .../(app)/compliance/readiness-item-card.tsx | 2 +- .../(app)/compliance/restrictions/page.tsx | 1 - .../app/(app)/compliance/settings/page.tsx | 1 - .../src/app/(app)/dashboard/page.tsx | 4 +- .../src/app/(app)/equipment/[id]/page.tsx | 1 - .../src/app/(app)/equipment/new/page.tsx | 1 - .../src/app/(app)/extractions/[id]/page.tsx | 1 - .../src/app/(app)/extractions/new/page.tsx | 1 - .../src/app/(app)/feedings/new/page.tsx | 1 - .../src/app/(app)/harvests/[id]/page.tsx | 1 - .../src/app/(app)/harvests/new/page.tsx | 2 +- .../[id]/honey-batch-movements-card.tsx | 2 +- .../(app)/inspections/new/hive-section.tsx | 2 +- .../src/app/(app)/inspections/new/page.tsx | 4 +- .../app/(app)/medicines/lots/[id]/page.tsx | 1 - ApiCore-Frontend/src/app/(app)/more-sheet.tsx | 2 +- .../src/app/(app)/offline/colony/page.tsx | 1 - .../(app)/production-batches/[id]/page.tsx | 1 - .../app/(app)/production-batches/new/page.tsx | 1 - .../src/app/(app)/products/[id]/page.tsx | 1 - .../src/app/(app)/products/new/page.tsx | 1 - .../(app)/profile/appearance-section.spec.tsx | 15 +- .../app/(app)/profile/appearance-section.tsx | 11 +- .../src/app/(app)/profile/page.tsx | 1 - .../src/app/(app)/queens/[id]/page.tsx | 1 - .../src/app/(app)/queens/new/page.tsx | 1 - .../src/app/(app)/raw-materials/[id]/page.tsx | 1 - .../src/app/(app)/raw-materials/new/page.tsx | 1 - .../settings/automation/api-key-allowlist.tsx | 4 +- .../settings/automation/api-keys-tab.tsx | 2 +- .../app/(app)/settings/automation/page.tsx | 1 - .../label-templates/designer/label-canvas.tsx | 4 +- .../settings/label-templates/new/page.tsx | 1 - .../app/(app)/settings/users-roles/page.tsx | 1 - .../src/app/(app)/settings/voice/page.tsx | 1 - .../src/app/(app)/tasks/new/page.tsx | 1 - .../src/app/(app)/treatments/new/page.tsx | 1 - ApiCore-Frontend/src/app/globals.css | 254 +++++++++--------- .../src/app/herkunft/[lot]/page.tsx | 8 +- .../src/components/apiary-map.tsx | 2 +- .../src/components/appearance-script.spec.tsx | 52 ++-- .../src/components/appearance-script.tsx | 2 +- .../src/components/area-migration.spec.ts | 6 +- .../components/autopilot/next-steps-card.tsx | 2 +- .../src/components/brand-mark.spec.tsx | 7 + .../src/components/brand-mark.tsx | 6 +- .../compliance/restriction-notice.tsx | 2 +- .../src/components/form/choice-group.tsx | 2 +- .../src/components/form/toggle-chip.tsx | 4 +- .../labels/label-compliance-panel.tsx | 2 +- .../components/offline/load-pack-button.tsx | 2 +- .../components/phenology/apiary-season.tsx | 2 +- .../components/rounds/round-next-panel.tsx | 2 +- .../src/components/stock/stock-card.tsx | 2 +- .../suggestions/suggestion-card.tsx | 2 +- .../suggestions/suggestion-group.tsx | 2 +- .../src/components/task-status.tsx | 2 +- .../components/today/inspection-due-card.tsx | 2 +- .../src/components/today/today-panel.tsx | 6 +- .../treatments/medicine-lot-fields.tsx | 4 +- ApiCore-Frontend/src/components/ui/avatar.tsx | 2 +- ApiCore-Frontend/src/components/ui/badge.tsx | 10 +- ApiCore-Frontend/src/components/ui/button.tsx | 10 +- .../src/components/ui/checkbox.tsx | 2 +- .../src/components/ui/dropdown-menu.tsx | 2 +- ApiCore-Frontend/src/components/ui/input.tsx | 2 +- ApiCore-Frontend/src/components/ui/select.tsx | 2 +- ApiCore-Frontend/src/components/ui/tabs.tsx | 6 +- .../src/components/ui/textarea.tsx | 2 +- .../src/components/ui/ui-colors.spec.tsx | 54 ++++ .../src/components/voice/voice-recorder.tsx | 4 +- .../src/lib/appearance/appearance-core.ts | 12 +- .../src/lib/appearance/appearance.spec.ts | 17 +- .../src/lib/appearance/appearance.ts | 9 +- ApiCore-Frontend/src/lib/areas.spec.ts | 86 +++++- ApiCore-Frontend/src/lib/colonies/labels.ts | 10 +- .../src/lib/forms/rating-contrast.spec.ts | 73 ----- ApiCore-Frontend/src/styles/contrast.spec.ts | 78 +++++- .../src/styles/css-tokens.spec.ts | 8 +- ApiCore-Frontend/src/styles/css-tokens.ts | 9 + 91 files changed, 673 insertions(+), 391 deletions(-) create mode 100644 ApiCore-Frontend/scripts/codemod-t5-colors.mjs create mode 100644 ApiCore-Frontend/scripts/codemod-t5-colors.spec.mjs create mode 100644 ApiCore-Frontend/src/components/ui/ui-colors.spec.tsx delete mode 100644 ApiCore-Frontend/src/lib/forms/rating-contrast.spec.ts diff --git a/ApiCore-Frontend/scripts/codemod-t5-colors.mjs b/ApiCore-Frontend/scripts/codemod-t5-colors.mjs new file mode 100644 index 0000000..609f29f --- /dev/null +++ b/ApiCore-Frontend/scripts/codemod-t5-colors.mjs @@ -0,0 +1,87 @@ +#!/usr/bin/env node +/** + * T5 colour codemod (spec 4.2 and 10.8 step 3). Pure and idempotent: + * + * - `text-primary` -> `text-primary-ink` (honey is a fill, the ink is the text colour); + * `text-primary-foreground`, `text-primary/NN`, `bg-primary…` stay. + * - `text-warning` -> `text-warning-ink`. + * - `text-warning-foreground` and `text-condition-x-foreground` -> `-ink`, unless the same line + * puts the text on the solid surface (`bg-warning`, `bg-condition-x` without `/NN`): on a tint + * or on the card only the ink reaches 4.5:1 in every set. + * - `border-primary` -> `border-foreground` where the line, or one of the two lines above it, + * names a selection state (checked, pressed, selected); `border-primary/NN` stays. + * + * CLI: node scripts/codemod-t5-colors.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"; + +/** Start of a class token: line start, whitespace, quote, backtick, `{`, `(` or a variant `:`. */ +const BEFORE = String.raw`(?<=^|[\s"'\`{(:!])`; +/** End of a class token: no further name characters, no `/NN` opacity, no `-suffix`. */ +const AFTER = String.raw`(?![\w/-])`; + +const TONES = String.raw`(?:warning|condition-(?:excellent|good|average|weak|critical))`; +const SELECTION_STATE = /checked|pressed|selected/; +const SELECTION_CONTEXT_LINES = 2; + +const TEXT_PRIMARY = new RegExp(`${BEFORE}text-primary${AFTER}`, "g"); +const TEXT_WARNING = new RegExp(`${BEFORE}text-warning${AFTER}`, "g"); +const TONE_FOREGROUND = new RegExp(`${BEFORE}text-(${TONES})-foreground${AFTER}`, "g"); +const BORDER_PRIMARY = new RegExp(`${BEFORE}border-primary${AFTER}`, "g"); + +function hasSolidSurface(line, tone) { + return new RegExp(`${BEFORE}bg-${tone}${AFTER}`).test(line); +} + +function transformLine(line, context) { + let out = line + .replace(TEXT_PRIMARY, "text-primary-ink") + .replace(TEXT_WARNING, "text-warning-ink") + .replace(TONE_FOREGROUND, (match, tone) => (hasSolidSurface(line, tone) ? match : `text-${tone}-ink`)); + if (SELECTION_STATE.test(context)) out = out.replace(BORDER_PRIMARY, "border-foreground"); + return out; +} + +/** Applies the colour rules to one source text; line endings are kept. */ +export function transform(source) { + const lines = source.split("\n"); + return lines + .map((line, index) => { + const context = lines.slice(Math.max(0, index - SELECTION_CONTEXT_LINES), index + 1).join("\n"); + return transformLine(line, context); + }) + .join("\n"); +} + +function sourceFiles(path) { + if (statSync(path).isDirectory()) { + return readdirSync(path).flatMap((name) => 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-colors.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-colors.spec.mjs b/ApiCore-Frontend/scripts/codemod-t5-colors.spec.mjs new file mode 100644 index 0000000..e1a77b6 --- /dev/null +++ b/ApiCore-Frontend/scripts/codemod-t5-colors.spec.mjs @@ -0,0 +1,98 @@ +import { readdirSync, readFileSync, statSync } from "node:fs"; +import { join } from "node:path"; +import { describe, expect, it } from "vitest"; +import { transform } from "./codemod-t5-colors.mjs"; + +describe("codemod t5 colours (spec 10.8 step 3)", () => { + it("text-primary becomes text-primary-ink", () => { + expect(transform('')).toBe( + '', + ); + expect(transform('cn("data-active:text-primary", x)')).toBe('cn("data-active:text-primary-ink", x)'); + }); + + it("text-primary-foreground, bg-primary and text-primary/80 stay unchanged", () => { + const source = 'className="bg-primary text-primary-foreground text-primary/80 text-primary/[0.07] bg-primary/10"'; + expect(transform(source)).toBe(source); + }); + + it("text-warning becomes text-warning-ink but text-warning-foreground stays on a solid warning surface", () => { + expect(transform('')).toBe( + '', + ); + const solid = 'className="bg-warning text-warning-foreground"'; + expect(transform(solid)).toBe(solid); + }); + + it("warning text on a tint or on the card becomes the ink", () => { + expect(transform('"bg-warning/15 text-warning-foreground"')).toBe('"bg-warning/15 text-warning-ink"'); + expect(transform('')).toBe(''); + }); + + it("badge classes with text-condition-good-foreground become -ink", () => { + expect(transform('GOOD: "border-transparent bg-condition-good/15 text-condition-good-foreground",')).toBe( + 'GOOD: "border-transparent bg-condition-good/15 text-condition-good-ink",', + ); + expect(transform('')).toBe( + '', + ); + }); + + it("text on a solid condition surface keeps its foreground", () => { + const solid = '4: "border-condition-good bg-condition-good text-condition-good-foreground",'; + expect(transform(solid)).toBe(solid); + }); + + it("border-primary on a selection state becomes border-foreground", () => { + expect(transform('"data-checked:border-primary data-checked:bg-primary"')).toBe( + '"data-checked:border-foreground data-checked:bg-primary"', + ); + expect(transform('className="has-[:checked]:border-primary has-[:checked]:bg-primary/10"')).toBe( + 'className="has-[:checked]:border-foreground has-[:checked]:bg-primary/10"', + ); + // The state may sit on the line(s) just above the class string. + expect(transform('selected &&\n "border-primary bg-primary font-medium",')).toBe( + 'selected &&\n "border-foreground bg-primary font-medium",', + ); + }); + + it("border-primary without a selection state and translucent borders stay", () => { + const plain = 'className={ok ? "rounded-lg border border-primary bg-primary/5" : "border-destructive"}'; + expect(transform(plain)).toBe(plain); + const translucent = 'className={selected ? "border-primary/40" : ""}'; + expect(transform(translucent)).toBe(translucent); + }); + + it("keeps CRLF line endings", () => { + expect(transform('a\r\nclassName="text-primary"\r\n')).toBe('a\r\nclassName="text-primary-ink"\r\n'); + }); + + it("is idempotent", () => { + const source = [ + 'className="text-primary text-warning bg-warning/15 text-warning-foreground"', + 'checked ? "border-primary bg-primary/10" : "border-border"', + '"bg-condition-weak/15 text-condition-weak-foreground"', + ].join("\n"); + const once = transform(source); + expect(once).not.toBe(source); + 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]/weather-card.tsx b/ApiCore-Frontend/src/app/(app)/apiaries/[id]/weather-card.tsx index a048297..b522706 100644 --- a/ApiCore-Frontend/src/app/(app)/apiaries/[id]/weather-card.tsx +++ b/ApiCore-Frontend/src/app/(app)/apiaries/[id]/weather-card.tsx @@ -126,8 +126,8 @@ export function WeatherCard({
{todaySuitability ? ( @@ -155,7 +155,7 @@ export function WeatherCard({ {day.tempMinC !== null ? `${Math.round(day.tempMinC)}°` : "–"} {day.precipitationMm !== null && day.precipitationMm > 0 ? ( - + {day.precipitationMm.toFixed(0)} mm ) : null} 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 4ed2cf5..af88514 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 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 focus-visible:outline-2 focus-visible:outline-ring"; type FixTarget = "master" | "product" | "designer"; @@ -301,7 +301,7 @@ export function PrintLabelsCard({
    {warnings.map((warning, index) => (
  • - + Hinweis: {warning.title} diff --git a/ApiCore-Frontend/src/app/(app)/bottling/new/page.tsx b/ApiCore-Frontend/src/app/(app)/bottling/new/page.tsx index 5ec2a9d..4f0698d 100644 --- a/ApiCore-Frontend/src/app/(app)/bottling/new/page.tsx +++ b/ApiCore-Frontend/src/app/(app)/bottling/new/page.tsx @@ -452,14 +452,14 @@ export default function NewBottlingBatchPage() { /> {withdrawalCheckFailed ? (

    - + Wartezeit konnte nicht geprüft werden

    ) : null} {withdrawalHints.length > 0 ? (

    - + Honig aus einer Wartezeit

      diff --git a/ApiCore-Frontend/src/app/(app)/bottom-nav.tsx b/ApiCore-Frontend/src/app/(app)/bottom-nav.tsx index d9aea36..12003bb 100644 --- a/ApiCore-Frontend/src/app/(app)/bottom-nav.tsx +++ b/ApiCore-Frontend/src/app/(app)/bottom-nav.tsx @@ -28,7 +28,7 @@ export function BottomNav({ user }: { user: Pick }) { aria-current={isActive ? "page" : undefined} className={cn( "flex flex-1 flex-col items-center justify-center gap-1 text-xs", - isActive ? "text-primary" : "text-muted-foreground", + isActive ? "text-primary-ink" : "text-muted-foreground", )} >
    {blocked?.index === index ? (

    - + {blocked.reason} Mach zuerst die späteren Änderungen rückgängig.

    ) : null} diff --git a/ApiCore-Frontend/src/app/(app)/inspections/new/page.tsx b/ApiCore-Frontend/src/app/(app)/inspections/new/page.tsx index 0905dac..8384ddf 100644 --- a/ApiCore-Frontend/src/app/(app)/inspections/new/page.tsx +++ b/ApiCore-Frontend/src/app/(app)/inspections/new/page.tsx @@ -734,7 +734,7 @@ function InspectionCaptureForm({ {noQueenSigns ? (

    Weder Königin noch Stifte gesehen. Nach dem Speichern wird @@ -894,7 +894,7 @@ function InspectionCaptureForm({ /> {values.feedLegacyText !== null ? (

    - + {feedLegacyHint(values.feedLegacyText)}

    ) : null} diff --git a/ApiCore-Frontend/src/app/(app)/medicines/lots/[id]/page.tsx b/ApiCore-Frontend/src/app/(app)/medicines/lots/[id]/page.tsx index 99517be..3beb6d7 100644 --- a/ApiCore-Frontend/src/app/(app)/medicines/lots/[id]/page.tsx +++ b/ApiCore-Frontend/src/app/(app)/medicines/lots/[id]/page.tsx @@ -3,7 +3,6 @@ import type { ReactNode } from "react"; import Link from "next/link"; import { useParams } from "next/navigation"; - import { useQuery } from "@tanstack/react-query"; import { useAuth } from "@/lib/auth/auth-context"; import { buildBook } from "@/lib/medicines/book"; diff --git a/ApiCore-Frontend/src/app/(app)/more-sheet.tsx b/ApiCore-Frontend/src/app/(app)/more-sheet.tsx index 908588c..7287279 100644 --- a/ApiCore-Frontend/src/app/(app)/more-sheet.tsx +++ b/ApiCore-Frontend/src/app/(app)/more-sheet.tsx @@ -37,7 +37,7 @@ export function MoreSheet({ user }: { user: Pick }) {