feat(t5): neue Farbwerte, Dunkel für alle, Farb-Codemod (T5a A4)

- 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) <[email protected]>
This commit is contained in:
Nick.RosenloecherandClaude Opus 5.5 committed 2026-10-16 17:23:21 +02:00
1 parent 6edae3efec
commit d8238855fc
91 files changed
+673 -391

No files matched your search

@@ -37,7 +37,7 @@ export function ApiaryMap({ apiaries }: ApiaryMapProps) {
{apiaries.map((apiary) => (
<Marker key={apiary.id} position={[apiary.latitude, apiary.longitude]} icon={markerIcon}>
<Popup>
<Link href={`/apiaries/${apiary.id}`} className="font-medium text-primary underline">
<Link href={`/apiaries/${apiary.id}`} className="font-medium text-primary-ink underline">
{apiary.name}
</Link>
{apiary.location ? <p className="text-sm text-muted-foreground">{apiary.location}</p> : null}
@@ -23,7 +23,7 @@ afterEach(() => {
describe("appearanceScript", () => {
it("uses the same storage keys as the hook", () => {
const source = appearanceScript(true);
const source = appearanceScript();
for (const key of Object.values(APPEARANCE_KEYS)) expect(source).toContain(key);
});
@@ -32,7 +32,7 @@ describe("appearanceScript", () => {
window.localStorage.setItem(APPEARANCE_KEYS.contrast, "sun");
window.localStorage.setItem(APPEARANCE_KEYS.motion, "subtle");
run(appearanceScript(true));
run(appearanceScript());
const root = document.documentElement;
expect(root).toHaveClass("dark");
@@ -44,29 +44,25 @@ describe("appearanceScript", () => {
const themes = [null, "light", "dark", "system", "bogus"];
const contrasts = [null, "normal", "sun", "bogus"];
const motions = [null, "full", "subtle", "off", "bogus"];
for (const darkEnabled of [true, false])
for (const prefersDark of [true, false])
for (const prefersMore of [true, false])
for (const theme of themes)
for (const contrast of contrasts)
for (const motion of motions) {
resetDocument();
stubMedia(prefersDark, prefersMore);
if (theme) window.localStorage.setItem(APPEARANCE_KEYS.theme, theme);
if (contrast) window.localStorage.setItem(APPEARANCE_KEYS.contrast, contrast);
if (motion) window.localStorage.setItem(APPEARANCE_KEYS.motion, motion);
for (const prefersDark of [true, false])
for (const prefersMore of [true, false])
for (const theme of themes)
for (const contrast of contrasts)
for (const motion of motions) {
resetDocument();
stubMedia(prefersDark, prefersMore);
if (theme) window.localStorage.setItem(APPEARANCE_KEYS.theme, theme);
if (contrast) window.localStorage.setItem(APPEARANCE_KEYS.contrast, contrast);
if (motion) window.localStorage.setItem(APPEARANCE_KEYS.motion, motion);
run(appearanceScript(darkEnabled));
run(appearanceScript());
const expected = resolveAppearance(
{ theme, contrast, motion },
{ prefersDark, prefersMore, darkEnabled },
);
const root = document.documentElement;
expect(root.classList.contains("dark")).toBe(expected.dark);
expect(root.dataset.contrast).toBe(expected.sun ? "sun" : "normal");
expect(root.dataset.motion).toBe(expected.motion);
}
const expected = resolveAppearance({ theme, contrast, motion }, { prefersDark, prefersMore });
const root = document.documentElement;
expect(root.classList.contains("dark")).toBe(expected.dark);
expect(root.dataset.contrast).toBe(expected.sun ? "sun" : "normal");
expect(root.dataset.motion).toBe(expected.motion);
}
});
it("blocked storage falls back to the default", () => {
@@ -78,7 +74,7 @@ describe("appearanceScript", () => {
},
});
try {
run(appearanceScript(true));
run(appearanceScript());
} finally {
Object.defineProperty(window, "localStorage", real);
}
@@ -102,14 +98,18 @@ describe("appearanceScript", () => {
for (const [theme, contrast, color] of cases) {
window.localStorage.setItem(APPEARANCE_KEYS.theme, theme);
window.localStorage.setItem(APPEARANCE_KEYS.contrast, contrast);
run(appearanceScript(true));
run(appearanceScript());
expect(meta.content).toBe(color);
}
});
it("has no build flag for dark any more (task A4)", () => {
expect(appearanceScript()).not.toMatch(/DARK=/);
});
it("contains no stored or user data, only constants", () => {
window.localStorage.setItem(APPEARANCE_KEYS.theme, "secret-value");
expect(appearanceScript(true)).not.toContain("secret-value");
expect(appearanceScript()).not.toContain("secret-value");
});
});
@@ -15,7 +15,7 @@ export function AppearanceScript() {
return (
<script
dangerouslySetInnerHTML={{
__html: appearanceScript(process.env.NEXT_PUBLIC_APPEARANCE_DARK === "1"),
__html: appearanceScript(),
}}
/>
);
@@ -27,10 +27,8 @@ describe("area migration", () => {
expect(offenders).toEqual([]);
});
it("no source other than globals.css uses the --gradient tokens", () => {
const offenders = sourceFiles(SRC)
.filter((file) => !file.endsWith("globals.css"))
.filter((file) => /--gradient-(honey|sage|sky)/.test(readFileSync(file, "utf8")));
it("no source uses or defines the --gradient tokens any more (removed in task A4)", () => {
const offenders = sourceFiles(SRC).filter((file) => /--gradient-/.test(readFileSync(file, "utf8")));
expect(offenders).toEqual([]);
});
@@ -157,7 +157,7 @@ export function NextStepsView({
className="flex flex-col gap-3 rounded-xl border border-border/60 p-3"
>
<div className="flex items-start gap-3">
<span className="flex size-10 shrink-0 items-center justify-center rounded-xl bg-primary/12 text-primary">
<span className="flex size-10 shrink-0 items-center justify-center rounded-xl bg-primary/12 text-primary-ink">
<Icon className="size-5" aria-hidden />
</span>
<div className="flex min-w-0 flex-1 flex-col gap-1.5">
@@ -13,6 +13,13 @@ describe("BrandMark", () => {
expect(screen.getByText("ApiCore")).toBeInTheDocument();
});
it("fills the cell with --primary (= spec honey #f0b13f, see contrast.spec) and the lid with wax", () => {
const { container } = render(<BrandMark />);
const [cell, lid] = container.querySelectorAll("svg path");
expect(cell).toHaveAttribute("fill", "var(--primary)");
expect(lid).toHaveAttribute("fill", "oklch(0.88 0.11 85)");
});
it("is decorative without a wordmark and sized 24 or 32 px", () => {
const { container, rerender } = render(<BrandMark />);
const svg = container.querySelector("svg") as SVGElement;
@@ -15,7 +15,11 @@ interface BrandMarkProps {
className?: string;
}
/** The ApiCore mark: a capped cell, optionally followed by the wordmark. */
/**
* The ApiCore mark: a capped cell, optionally followed by the wordmark. The cell is --primary:
* its light value is the spec honey #f0b13f (pinned in contrast.spec), dark differs by one step
* (#efb146), so no separate brand token is needed. Static files (task A9) use the hex directly.
*/
export function BrandMark({ size = 24, withWordmark = false, className }: BrandMarkProps) {
const mark = (
<svg
@@ -118,7 +118,7 @@ export function HarvestRestrictionWarning({ apiaryId }: { apiaryId: string }) {
<div aria-live="polite">
{restricted ? (
<p className="flex items-start gap-2 rounded-lg bg-warning/15 p-3 text-sm">
<AlertTriangle className="mt-0.5 size-4 shrink-0 text-warning" aria-hidden />
<AlertTriangle className="mt-0.5 size-4 shrink-0 text-warning-ink" aria-hidden />
<span>Im Sperrbezirk gelten Auflagen auch für Honig und Waben. Verfügung prüfen.</span>
</p>
) : null}
@@ -142,7 +142,7 @@ export function ChoiceGroup<T extends ChoiceValue>({
selected && isUnanswered && "border-foreground/30 bg-muted font-medium text-foreground",
selected &&
!isUnanswered &&
"border-primary bg-primary font-medium text-primary-foreground",
"border-foreground bg-primary font-medium text-primary-foreground",
selected && option.selectedClassName,
optionClassName,
)}
@@ -25,7 +25,7 @@ export function ToggleChip({ label, checked, onChange, describedBy, disabled }:
className={cn(
"flex min-h-11 items-center gap-2 rounded-lg border px-3 text-left text-sm motion-safe:transition-colors disabled:pointer-events-none disabled:opacity-50",
checked
? "border-primary bg-primary/10 font-medium"
? "border-foreground bg-primary/10 font-medium"
: "border-border bg-background hover:bg-muted",
)}
>
@@ -34,7 +34,7 @@ export function ToggleChip({ label, checked, onChange, describedBy, disabled }:
className={cn(
"flex size-5 shrink-0 items-center justify-center rounded border",
checked
? "border-primary bg-primary text-primary-foreground"
? "border-foreground bg-primary text-primary-foreground"
: "border-border",
)}
>
@@ -121,7 +121,7 @@ export function LabelCompliancePanel({
<ul aria-label="Hinweise" className="flex flex-col gap-2">
{result.warnings.map((warning, index) => (
<li key={`${warning.code}-${index}`} className="flex items-start gap-2 text-sm">
<TriangleAlert aria-hidden className="mt-0.5 size-4 shrink-0 text-warning" />
<TriangleAlert aria-hidden className="mt-0.5 size-4 shrink-0 text-warning-ink" />
<span className="flex flex-col">
<span className="font-medium">Hinweis: {warning.title}</span>
{warning.detail ? (
@@ -37,7 +37,7 @@ export function PackAgeNote({ stored, now }: PackAgeNoteProps) {
<p className="text-muted-foreground">Geladen {packAgeText(stored.savedAt, now)}</p>
{isPackStale(stored, now) ? (
<p className="flex items-center gap-1.5 font-medium">
<AlertTriangle className="size-4 shrink-0 text-warning" aria-hidden />
<AlertTriangle className="size-4 shrink-0 text-warning-ink" aria-hidden />
{`Stand-Daten von ${packDayText(stored.savedAt)} – bei Netz neu laden`}
</p>
) : null}
@@ -48,7 +48,7 @@ export function ApiarySeason({ apiary }: ApiarySeasonProps) {
</>
) : null}
{state.note ? (
<span className="text-warning-foreground">{SEASON_NOTE_LABELS[state.note]}</span>
<span className="text-warning-ink">{SEASON_NOTE_LABELS[state.note]}</span>
) : null}
</div>
) : null}
@@ -21,7 +21,7 @@ export function RoundNextPanel({ next, doneCount, savedOffline, onNext, onEnd }:
<Card className="max-w-lg">
<CardContent className="flex flex-col gap-4">
<p role="status" className="flex items-center gap-2 text-lg font-medium">
<CheckCircle2 className="size-5 text-condition-good-foreground" aria-hidden />
<CheckCircle2 className="size-5 text-condition-good-ink" aria-hidden />
Gespeichert
</p>
{savedOffline ? (
@@ -190,7 +190,7 @@ function MovementList({ movements }: { movements: StockMovement[] }) {
className={
movement.quantity < 0
? "font-semibold tabular-nums text-destructive"
: "font-semibold tabular-nums text-primary"
: "font-semibold tabular-nums text-primary-ink"
}
>
{movement.quantity > 0 ? "+" : ""}
@@ -109,7 +109,7 @@ export function SuggestionCard(props: SuggestionCardProps) {
return (
<article className="flex min-w-0 flex-col gap-3 rounded-2xl border bg-card p-4 shadow-[var(--shadow-soft)]">
<div className="flex items-start gap-3">
<span className="flex size-10 shrink-0 items-center justify-center rounded-xl bg-primary/12 text-primary">
<span className="flex size-10 shrink-0 items-center justify-center rounded-xl bg-primary/12 text-primary-ink">
<Icon className="size-5" aria-hidden />
</span>
<div className="flex min-w-0 flex-1 flex-col gap-1.5">
@@ -62,7 +62,7 @@ function CombinedRow({
return (
<div className="flex flex-col gap-2 rounded-2xl border bg-card p-4 shadow-[var(--shadow-soft)]">
<div className="flex flex-wrap items-center gap-3">
<span className="flex size-10 shrink-0 items-center justify-center rounded-xl bg-primary/12 text-primary">
<span className="flex size-10 shrink-0 items-center justify-center rounded-xl bg-primary/12 text-primary-ink">
<Icon className="size-5" aria-hidden />
</span>
<h3 className="min-w-0 flex-1 font-medium">
@@ -56,7 +56,7 @@ export function TaskSourceBadge({
title={source.title}
className="inline-flex items-center gap-1 rounded-full border border-dashed border-primary/45 px-2 py-0.5 text-xs text-foreground/80"
>
<Icon className="size-3.5 text-primary" aria-hidden />
<Icon className="size-3.5 text-primary-ink" aria-hidden />
{source.label}
</span>
);
@@ -40,7 +40,7 @@ export function InspectionDueCard({ entry, primary = true }: InspectionDueCardPr
className="flex flex-col gap-3 rounded-2xl border border-border/60 bg-card p-4 shadow-[var(--shadow-soft)] sm:p-5"
>
<div className="flex flex-wrap items-center gap-x-3 gap-y-2">
<span className="flex size-10 shrink-0 items-center justify-center rounded-xl bg-primary/12 text-primary">
<span className="flex size-10 shrink-0 items-center justify-center rounded-xl bg-primary/12 text-primary-ink">
<ClipboardCheck className="size-5" aria-hidden />
</span>
<h3 id={headingId} className="min-w-0 flex-1 font-medium">
@@ -157,7 +157,7 @@ export function TodayPanel({
</h2>
<Link
href="/tasks"
className="inline-flex min-h-11 items-center text-sm font-medium text-primary underline-offset-4 hover:underline"
className="inline-flex min-h-11 items-center text-sm font-medium text-primary-ink underline-offset-4 hover:underline"
>
Alle Aufgaben
</Link>
@@ -307,7 +307,7 @@ function AutopilotStepCard({ step, busy, ...handlers }: AutopilotStepCardProps)
<div key={step.key} className="next-step-enter mt-2 flex flex-col gap-4">
<div className="flex flex-col gap-2">
<p className="flex items-start gap-3 font-heading text-2xl font-semibold leading-tight tracking-tight text-balance sm:text-3xl">
<Icon className="mt-1 size-6 shrink-0 text-primary" aria-hidden />
<Icon className="mt-1 size-6 shrink-0 text-primary-ink" aria-hidden />
{step.title}
</p>
<div className="flex flex-wrap items-center gap-2">
@@ -445,7 +445,7 @@ function TodayRow({ task, now, leaving, colonyCode, onComplete }: TodayRowProps)
function NothingDue() {
return (
<section className="flex flex-col items-start gap-4 rounded-2xl border border-border/60 bg-card p-5 shadow-[var(--shadow-soft)] sm:p-6">
<span className="grid size-11 place-items-center rounded-full bg-primary/12 text-primary">
<span className="grid size-11 place-items-center rounded-full bg-primary/12 text-primary-ink">
<Sun className="size-6" aria-hidden />
</span>
<div className="flex flex-col gap-1">
@@ -321,7 +321,7 @@ export function BookingNotes({
) : null}
{warnings.map((warning) => (
<p key={warning} className="flex items-start gap-2 text-sm font-medium">
<AlertTriangle className="mt-0.5 size-4 shrink-0 text-warning" aria-hidden />
<AlertTriangle className="mt-0.5 size-4 shrink-0 text-warning-ink" aria-hidden />
<span>{warning}</span>
</p>
))}
@@ -366,7 +366,7 @@ function TemperatureNote({
const Icon = hint.warn ? AlertTriangle : Thermometer;
return (
<p data-warn={hint.warn} className="flex flex-wrap items-start gap-x-2 text-sm">
<Icon className={`mt-0.5 size-4 shrink-0 ${hint.warn ? "text-warning" : ""}`} aria-hidden />
<Icon className={`mt-0.5 size-4 shrink-0 ${hint.warn ? "text-warning-ink" : ""}`} aria-hidden />
<span>{hint.text}</span>
{hint.warn ? <span className="font-medium">Außerhalb des Katalog-Bereichs</span> : null}
</p>
@@ -17,7 +17,7 @@ function Avatar({
data-slot="avatar"
data-size={size}
className={cn(
"group/avatar relative flex size-8 shrink-0 rounded-full select-none after:absolute after:inset-0 after:rounded-full after:border after:border-border after:mix-blend-darken data-[size=lg]:size-10 data-[size=sm]:size-6 dark:after:mix-blend-lighten",
"group/avatar relative flex size-8 shrink-0 rounded-full select-none after:absolute after:inset-0 after:rounded-full after:border after:border-border data-[size=lg]:size-10 data-[size=sm]:size-6",
className
)}
{...props}
+5 -5
View File
@@ -5,7 +5,7 @@ import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const badgeVariants = cva(
"group/badge inline-flex h-5.5 w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-4xl border border-transparent px-2.5 py-0.5 text-xs font-semibold whitespace-nowrap transition-all focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3!",
"group/badge inline-flex h-5.5 w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-4xl border border-transparent px-2.5 py-0.5 text-xs font-semibold whitespace-nowrap transition-all focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 aria-invalid:border-destructive aria-invalid:ring-destructive/20 [&>svg]:pointer-events-none [&>svg]:size-3!",
{
variants: {
variant: {
@@ -13,14 +13,14 @@ const badgeVariants = cva(
secondary:
"bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80",
destructive:
"bg-destructive/10 text-destructive focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:focus-visible:ring-destructive/40 [a]:hover:bg-destructive/20",
"bg-destructive/10 text-destructive focus-visible:ring-destructive/20 [a]:hover:bg-destructive/20",
warning:
"bg-warning/15 text-warning-foreground focus-visible:ring-warning/30 [a]:hover:bg-warning/25",
"bg-warning/15 text-warning-ink focus-visible:ring-warning/30 [a]:hover:bg-warning/25",
outline:
"border-border text-foreground [a]:hover:bg-muted [a]:hover:text-muted-foreground",
ghost:
"hover:bg-muted hover:text-muted-foreground dark:hover:bg-muted/50",
link: "text-primary underline-offset-4 hover:underline",
"hover:bg-muted hover:text-muted-foreground",
link: "text-primary-ink underline-offset-4 hover:underline",
},
},
defaultVariants: {
@@ -4,21 +4,21 @@ import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"group/button inline-flex shrink-0 items-center justify-center rounded-xl border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
"group/button inline-flex shrink-0 items-center justify-center rounded-xl border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
{
variants: {
variant: {
default:
"bg-primary text-primary-foreground shadow-[var(--shadow-soft)] hover:shadow-[var(--shadow-elevated)] hover:brightness-105 active:brightness-95",
outline:
"border-border bg-background hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50",
"border-border bg-background hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground",
secondary:
"bg-secondary text-secondary-foreground hover:bg-[color-mix(in_oklch,var(--secondary),var(--foreground)_5%)] aria-expanded:bg-secondary aria-expanded:text-secondary-foreground",
ghost:
"hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50",
"hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground",
destructive:
"bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40",
link: "text-primary underline-offset-4 hover:underline",
"bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20",
link: "text-primary-ink underline-offset-4 hover:underline",
},
size: {
// Phones get at least 40 px: tapped with a thumb, often with gloves.
@@ -8,7 +8,7 @@ function Checkbox({ className, ...props }: CheckboxPrimitive.Root.Props) {
<CheckboxPrimitive.Root
data-slot="checkbox"
className={cn(
"peer flex size-4 shrink-0 items-center justify-center rounded-[6px] border border-input bg-background shadow-xs outline-none transition-colors focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 data-checked:border-primary data-checked:bg-primary data-checked:text-primary-foreground",
"peer flex size-4 shrink-0 items-center justify-center rounded-[6px] border border-input bg-background shadow-xs outline-none transition-colors focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 data-checked:border-foreground data-checked:bg-primary data-checked:text-primary-foreground",
className
)}
{...props}
@@ -88,7 +88,7 @@ function DropdownMenuItem({
data-inset={inset}
data-variant={variant}
className={cn(
"group/dropdown-menu-item relative flex cursor-default items-center gap-1.5 rounded-md px-1.5 py-1 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground data-inset:pl-7 data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 data-[variant=destructive]:focus:text-destructive dark:data-[variant=destructive]:focus:bg-destructive/20 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 data-[variant=destructive]:*:[svg]:text-destructive",
"group/dropdown-menu-item relative flex cursor-default items-center gap-1.5 rounded-md px-1.5 py-1 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground data-inset:pl-7 data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 data-[variant=destructive]:focus:text-destructive data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 data-[variant=destructive]:*:[svg]:text-destructive",
className
)}
{...props}
+1 -1
View File
@@ -9,7 +9,7 @@ function Input({ className, type, ...props }: React.ComponentProps<"input">) {
type={type}
data-slot="input"
className={cn(
"h-11 w-full min-w-0 sm:h-9 rounded-xl border border-input bg-card px-3 py-1 text-base shadow-xs transition-all outline-none file:inline-flex file:h-6 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground hover:border-foreground/20 focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:pointer-events-none disabled:cursor-not-allowed disabled:bg-input/50 disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 md:text-sm dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40",
"h-11 w-full min-w-0 sm:h-9 rounded-xl border border-input bg-card px-3 py-1 text-base shadow-xs transition-all outline-none file:inline-flex file:h-6 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground hover:border-foreground/20 focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:pointer-events-none disabled:cursor-not-allowed disabled:bg-muted disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 md:text-sm",
className
)}
{...props}
@@ -41,7 +41,7 @@ function SelectTrigger({
data-slot="select-trigger"
data-size={size}
className={cn(
"flex w-fit items-center justify-between gap-1.5 rounded-xl border border-input bg-card py-2 pr-2.5 pl-3 text-sm whitespace-nowrap shadow-xs transition-all outline-none select-none hover:border-foreground/20 focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 data-placeholder:text-muted-foreground data-[size=default]:h-11 sm:data-[size=default]:h-9 data-[size=sm]:h-7 data-[size=sm]:rounded-[min(var(--radius-md),10px)] *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-1.5 dark:bg-input/30 dark:hover:bg-input/50 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
"flex w-fit items-center justify-between gap-1.5 rounded-xl border border-input bg-card py-2 pr-2.5 pl-3 text-sm whitespace-nowrap shadow-xs transition-all outline-none select-none hover:border-foreground/20 focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 data-placeholder:text-muted-foreground data-[size=default]:h-11 sm:data-[size=default]:h-9 data-[size=sm]:h-7 data-[size=sm]:rounded-[min(var(--radius-md),10px)] *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-1.5 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
+3 -3
View File
@@ -58,9 +58,9 @@ function TabsTrigger({ className, ...props }: TabsPrimitive.Tab.Props) {
<TabsPrimitive.Tab
data-slot="tabs-trigger"
className={cn(
"relative inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-lg border border-transparent px-2.5 py-0.5 text-sm font-medium whitespace-nowrap text-foreground/60 transition-all group-data-vertical/tabs:w-full group-data-vertical/tabs:justify-start hover:text-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-1 focus-visible:outline-ring disabled:pointer-events-none disabled:opacity-50 has-data-[icon=inline-end]:pr-1 has-data-[icon=inline-start]:pl-1 aria-disabled:pointer-events-none aria-disabled:opacity-50 dark:text-muted-foreground dark:hover:text-foreground group-data-[variant=default]/tabs-list:data-active:shadow-[var(--shadow-soft)] group-data-[variant=line]/tabs-list:data-active:shadow-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
"group-data-[variant=line]/tabs-list:bg-transparent group-data-[variant=line]/tabs-list:data-active:bg-transparent dark:group-data-[variant=line]/tabs-list:data-active:border-transparent dark:group-data-[variant=line]/tabs-list:data-active:bg-transparent",
"data-active:bg-card data-active:text-primary dark:data-active:border-input dark:data-active:bg-input/30 dark:data-active:text-foreground",
"relative inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-lg border border-transparent px-2.5 py-0.5 text-sm font-medium whitespace-nowrap text-foreground/60 transition-all group-data-vertical/tabs:w-full group-data-vertical/tabs:justify-start hover:text-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-1 focus-visible:outline-ring disabled:pointer-events-none disabled:opacity-50 has-data-[icon=inline-end]:pr-1 has-data-[icon=inline-start]:pl-1 aria-disabled:pointer-events-none aria-disabled:opacity-50 group-data-[variant=default]/tabs-list:data-active:shadow-[var(--shadow-soft)] group-data-[variant=line]/tabs-list:data-active:shadow-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
"group-data-[variant=line]/tabs-list:bg-transparent group-data-[variant=line]/tabs-list:data-active:bg-transparent",
"data-active:bg-card data-active:text-primary-ink",
"after:absolute after:bg-foreground after:opacity-0 after:transition-opacity group-data-horizontal/tabs:after:inset-x-0 group-data-horizontal/tabs:after:bottom-[-5px] group-data-horizontal/tabs:after:h-0.5 group-data-vertical/tabs:after:inset-y-0 group-data-vertical/tabs:after:-right-1 group-data-vertical/tabs:after:w-0.5 group-data-[variant=line]/tabs-list:data-active:after:opacity-100",
className
)}
@@ -7,7 +7,7 @@ function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
<textarea
data-slot="textarea"
className={cn(
"flex field-sizing-content min-h-16 w-full rounded-xl border border-input bg-card px-3 py-2 text-base shadow-xs transition-all outline-none placeholder:text-muted-foreground hover:border-foreground/20 focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:bg-input/50 disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 md:text-sm dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40",
"flex field-sizing-content min-h-16 w-full rounded-xl border border-input bg-card px-3 py-2 text-base shadow-xs transition-all outline-none placeholder:text-muted-foreground hover:border-foreground/20 focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:bg-muted disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 md:text-sm",
className
)}
{...props}
@@ -0,0 +1,54 @@
import { readdirSync, readFileSync, statSync } from "node:fs";
import { join } from "node:path";
import { render, screen } from "@testing-library/react";
import { describe, expect, it } from "vitest";
import { Badge } from "./badge";
import { Button } from "./button";
const SRC = join(__dirname, "..", "..");
function sourceFiles(dir: string): string[] {
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] : [];
});
}
/** A Tailwind class with the dark variant, e.g. `dark:bg-input/30` (not an object key `dark: x`). */
const DARK_CLASS = /(^|[\s"'`])dark:[^\s"'`]/m;
describe("ui colours after the value switch (spec 10.8 step 4)", () => {
it("the default button uses solid primary with primary-foreground and no gradient", () => {
render(<Button>Speichern</Button>);
const classes = screen.getByRole("button", { name: "Speichern" }).className;
expect(classes).toMatch(/(^|\s)bg-primary(\s|$)/);
expect(classes).toMatch(/(^|\s)text-primary-foreground(\s|$)/);
expect(classes).not.toMatch(/gradient|background-image/);
});
it("link-style buttons and badges use the primary ink, not the honey fill colour, for text", () => {
render(
<>
<Button variant="link">Mehr</Button>
<Badge variant="link">Link</Badge>
<Badge variant="warning">Achtung</Badge>
</>,
);
expect(screen.getByRole("button", { name: "Mehr" }).className).toMatch(/(^|\s)text-primary-ink(\s|$)/);
expect(screen.getByText("Link").className).toMatch(/(^|\s)text-primary-ink(\s|$)/);
expect(screen.getByText("Achtung").className).toMatch(/(^|\s)text-warning-ink(\s|$)/);
});
it("ui components contain no dark: classes", () => {
const offenders = sourceFiles(join(SRC, "components", "ui")).filter((file) =>
DARK_CLASS.test(readFileSync(file, "utf8")),
);
expect(offenders).toEqual([]);
});
it("no source file uses dark: classes - a mode is only another token set", () => {
const offenders = sourceFiles(SRC).filter((file) => DARK_CLASS.test(readFileSync(file, "utf8")));
expect(offenders).toEqual([]);
});
});
@@ -254,7 +254,7 @@ export function VoiceRecorder({
>
{rejected ? null : (
<CheckCircle2
className="mt-0.5 size-5 shrink-0 text-condition-good-foreground"
className="mt-0.5 size-5 shrink-0 text-condition-good-ink"
aria-hidden
/>
)}
@@ -311,7 +311,7 @@ export function VoiceRecorder({
{queuedNoteId ? (
<p role="status" className="flex items-start gap-2 text-base font-medium">
<CheckCircle2
className="mt-0.5 size-5 shrink-0 text-condition-good-foreground"
className="mt-0.5 size-5 shrink-0 text-condition-good-ink"
aria-hidden
/>
{QUEUED_TEXT}