feat(t5): Auswahlgruppe mit Haken, Karten-Variante, Plus/Minus (T5a A6)

- ChoiceGroup nach Spec 10.2: gewählt mit Haken und 2-px-Tintenrahmen,
  „Nicht geprüft“ gestrichelt; neue Variante appearance="cards" mit Icon
  und Beschreibung (Winter-Ergebnis, Darstellung, Behördenmeldung).
- Eigenbau-Radios in Behördenmeldung, Phänologie-Bericht und
  Zeilen-Ausrichtung durch ChoiceGroup ersetzt.
- Button pressed → aria-pressed (Fett/Kursiv, „Aufbau als Liste“);
  Druck-Versatz nur mit motion-full.
- NumberStepper (ganze Schritte) für Winter-Waben, Milbenzahl (neu und
  bearbeiten), Weiselzellen, Entnahme/Zählbestand, Startposition; Entnahme
  nur 1 bis Restmenge, Startposition höchstens 1000.
- Baukasten-Wächter: Radios nur in ChoiceGroup, aria-pressed nur in
  Button und Kalender, auch in Objektform und .ts-Dateien.

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 18:44:31 +02:00
1 parent 6ac9eef93b
commit 208e9baaf0
30 files changed
+809 -198

No files matched your search

@@ -81,6 +81,38 @@ const RULES: GuardRule[] = [
scope: "src/**/*.{ts,tsx}",
allow: [],
},
// Task A6: one radio group for the whole app (spec 10.2, 10.7).
{
id: "no role radio, radiogroup or type radio outside choice-group.tsx",
// JSX (role="radio", type={"radio"}) and object or spread form (role: "radio"); a CSS
// attribute selector ([role="radio"]) only finds ChoiceGroup radios and is no own build.
pattern: /(?<![\w[-])(?:role|type)\s*[=:]\s*\{?\s*["'`]radio(?:group)?["'`]/,
scope: "src/**/*.{ts,tsx}",
allow: [
{
file: "src/components/form/choice-group.tsx",
reason: "ChoiceGroup is the radio group of the kit: role radiogroup with one role radio per option.",
},
],
},
// Task A6: a toggle button sets aria-pressed through Button `pressed` (spec 10.2, 10.7).
{
id: "aria-pressed only through Button pressed",
// JSX attribute or object/spread key ("aria-pressed": x); not the Tailwind variant aria-pressed:.
pattern: /aria-pressed\s*=|["'`]aria-pressed["'`]\s*:/,
scope: "src/**/*.{ts,tsx}",
allow: [
{
file: "src/components/ui/button.tsx",
reason: "Button turns its `pressed` prop into aria-pressed (toolbar toggles, not a radio group).",
},
{
file: "src/app/(app)/calendar/page.tsx",
reason:
"The month grid is a grid of day buttons; the chosen day is pressed. It keeps the global focus and the selection ring (spec 10.2).",
},
],
},
];
const SCOPE_FILE: Record<GuardScope, RegExp> = {
@@ -161,6 +193,21 @@ describe("baukasten guard (spec 10.7)", () => {
expect(ownOutline.test('"p-3 focus:outline-2 focus:outline-ring"')).toBe(true);
expect(ownOutline.test('"focus-visible:outline-2"')).toBe(true);
expect(ownOutline.test('"outline-hidden focus:bg-accent"')).toBe(false);
const radio = rule("no role radio, radiogroup or type radio outside choice-group.tsx").pattern;
expect(radio.test('<input type="radio" name="x" />')).toBe(true);
expect(radio.test('<div role="radiogroup">')).toBe(true);
expect(radio.test("<button role={'radio'}>")).toBe(true);
expect(radio.test('<DropdownMenuRadioItem value="a">')).toBe(false);
const pressed = rule("aria-pressed only through Button pressed").pattern;
expect(pressed.test("<button aria-pressed={on}>")).toBe(true);
expect(pressed.test('"aria-pressed:bg-holo-edge/25"')).toBe(false);
expect(pressed.test('{ "aria-pressed": on }')).toBe(true);
expect(pressed.test("{ 'aria-pressed': on }")).toBe(true);
expect(radio.test('{ role: "radio" }')).toBe(true);
expect(radio.test("{ role: 'radiogroup' }")).toBe(true);
expect(radio.test('{ type: "radio" }')).toBe(true);
expect(radio.test('{ role: "radioactive" }')).toBe(false);
expect(radio.test(`querySelector('[role="radio"][tabindex="0"]')`)).toBe(false);
expect(rule("no text-white").pattern.test('"text-white/80"')).toBe(true);
expect(rule("no gradient prop or gradient token").pattern.test("<PageHeader gradient=\"honey\" />")).toBe(true);
});