feat(t5): ein Checkbox-Baustein für alle 35 Stellen (T5a A7)
- ui/checkbox.tsx und form/checkbox-field.tsx: ganze Zeile, Kachel, Aufgabe oder Sheet-Zeile ist der Knopf (Base UI), Haken bzw. Strich für teilweise; role="switch" mit Daumen und Haken. ToggleChip und SwitchRow entfernt. - 14 Kacheln, 11 native Checkboxen, 3 Schalter-Zeilen, 6 Base-UI-Stellen und die Heute-Aufgaben umgestellt; Werte ans Backend unverändert. - Enter in einer Checkbox schickt kein Formular ab und schaltet nicht (Leertaste schaltet); Nachbarn mit mindestens 4 px Abstand; gesperrte Felder mit sichtbarem Grund; gesperrte Kacheln ganz gedimmt. - Baukasten-Wächter: keine eigenen Checkboxen/Schalter, auch bedingte role-Werte und setAttribute; Zeilenlisten mit Abstand. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
1 parent
11bccbeb50
commit
f307e7da66
40 files changed
+1179
-409
No files matched your search
@@ -19,6 +19,20 @@ interface GuardRule {
|
||||
const FRONTEND = join(__dirname, "..", "..");
|
||||
const SRC = join(FRONTEND, "src");
|
||||
|
||||
/**
|
||||
* `keys` (role, type) set to `value` by hand: JSX (role="x", role={on ? "x" : y}), object or
|
||||
* spread form (role: "x") and setAttribute("role", "x"). A CSS selector ([role="x"]) only finds
|
||||
* kit controls, and the `role` prop inside a <CheckboxField> tag is the kit's own API.
|
||||
*/
|
||||
function ownRole(keys: string, value: string): RegExp {
|
||||
const quote = "[\"'`]";
|
||||
const quoted = `${quote}${value}${quote}`;
|
||||
// role="x" | role={... "x" ...} | role: ... "x" (not role?: or role === "x")
|
||||
const attribute = String.raw`(?<![\w[-])(?:${keys})\s*(?:=\s*(?:\{[^}]*?)?|:\s*[^,;}\n]*?)${quoted}`;
|
||||
const setAttribute = String.raw`setAttribute\(\s*${quote}(?:${keys})${quote}\s*,\s*${quoted}`;
|
||||
return new RegExp(String.raw`(?<!<CheckboxField\b[^<>]*)(?:${attribute}|${setAttribute})`);
|
||||
}
|
||||
|
||||
const RULES: GuardRule[] = [
|
||||
// Task A3/A4: the area tone replaced the gradients (spec 11); the gradient tokens are gone.
|
||||
{
|
||||
@@ -95,6 +109,23 @@ const RULES: GuardRule[] = [
|
||||
},
|
||||
],
|
||||
},
|
||||
// Task A7: one checkbox for the whole app (spec 10.3, 10.7). Base UI draws role checkbox and
|
||||
// its hidden input inside ui/checkbox.tsx, so no source file writes them by hand and the list
|
||||
// of exceptions stays empty. The `role` prop of <CheckboxField> picks the kit's own control
|
||||
// and is no own build (only within that same tag, so put `role` before any arrow handler).
|
||||
{
|
||||
id: "no role checkbox or type checkbox outside ui/checkbox.tsx",
|
||||
pattern: ownRole("role|type", "checkbox"),
|
||||
scope: "src/**/*.{ts,tsx}",
|
||||
allow: [],
|
||||
},
|
||||
// Task A7: a yes/no setting is <CheckboxField role="switch"> on Base UI Switch (ui/switch.tsx).
|
||||
{
|
||||
id: "no role switch outside ui/checkbox.tsx and ui/switch.tsx",
|
||||
pattern: ownRole("role", "switch"),
|
||||
scope: "src/**/*.{ts,tsx}",
|
||||
allow: [],
|
||||
},
|
||||
// Task A6: a toggle button sets aria-pressed through Button `pressed` (spec 10.2, 10.7).
|
||||
{
|
||||
id: "aria-pressed only through Button pressed",
|
||||
@@ -167,6 +198,30 @@ describe("baukasten guard (spec 10.7)", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("ToggleChip and SwitchRow are gone (CheckboxField tile and switch replace them)", () => {
|
||||
for (const name of ["toggle-chip", "switch-row"]) {
|
||||
expect(existsSync(join(SRC, "components", "form", `${name}.tsx`)), name).toBe(false);
|
||||
}
|
||||
const users = FILES.filter(({ text }) => /\b(?:ToggleChip|SwitchRow)\b|form\/(?:toggle-chip|switch-row)/.test(text));
|
||||
expect(users.map(({ file }) => file)).toEqual([]);
|
||||
});
|
||||
|
||||
it("a list of CheckboxFields keeps neighbouring targets at least 4 px apart (spec 4.5)", () => {
|
||||
// `.map(… => <CheckboxField` (optionally inside an <li>) and the container opened before it.
|
||||
const lists = FILES.filter(({ file }) => file.endsWith(".tsx")).flatMap(({ file, text }) =>
|
||||
[...text.matchAll(/\.map\(\s*\([^)]*\)\s*=>\s*\(?\s*(?:<li\b[^>]*>\s*)?<CheckboxField\b/g)].map((match) => {
|
||||
const before = text.slice(0, match.index);
|
||||
const containers = [...before.matchAll(/<(?:div|ul|fieldset)\b[^>]*?className="([^"]*)"/g)];
|
||||
const classes = containers.at(-1)?.[1] ?? "";
|
||||
const gap = /(?:^|\s)gap-(?:y-)?(\d+(?:\.5)?)(?=\s|$)/.exec(classes)?.[1];
|
||||
const line = before.split("\n").length;
|
||||
return { where: `${file}:${line}`, gapPx: gap === undefined ? 0 : Number(gap) * 4 };
|
||||
}),
|
||||
);
|
||||
expect(lists.length).toBeGreaterThanOrEqual(8);
|
||||
expect(lists.filter(({ gapPx }) => gapPx < 4).map(({ where, gapPx }) => `${where}: ${gapPx} px`)).toEqual([]);
|
||||
});
|
||||
|
||||
it("no component removes the outline without the global rule", () => {
|
||||
expect(GLOBALS).toMatch(/:where\(:focus-visible\)\s*\{[^}]*outline:\s*var\(--focus-width\)\s+solid\s+var\(--ring\)/);
|
||||
for (const id of ["no ring-ring/50", "no outline-none"]) {
|
||||
@@ -208,6 +263,30 @@ describe("baukasten guard (spec 10.7)", () => {
|
||||
expect(radio.test('{ type: "radio" }')).toBe(true);
|
||||
expect(radio.test('{ role: "radioactive" }')).toBe(false);
|
||||
expect(radio.test(`querySelector('[role="radio"][tabindex="0"]')`)).toBe(false);
|
||||
const checkbox = rule("no role checkbox or type checkbox outside ui/checkbox.tsx").pattern;
|
||||
expect(checkbox.test('<input type="checkbox" checked={on} />')).toBe(true);
|
||||
expect(checkbox.test('<button type="button" role="checkbox" aria-checked={on}>')).toBe(true);
|
||||
expect(checkbox.test("<span role={'checkbox'}>")).toBe(true);
|
||||
expect(checkbox.test('{ type: "checkbox" }')).toBe(true);
|
||||
expect(checkbox.test(`querySelector('[role="checkbox"]')`)).toBe(false);
|
||||
expect(checkbox.test('<CheckboxField label="Aktiv" role="checkbox" checked={on} />')).toBe(false);
|
||||
expect(checkbox.test('role?: "checkbox" | "switch"')).toBe(false);
|
||||
const toggle = rule("no role switch outside ui/checkbox.tsx and ui/switch.tsx").pattern;
|
||||
expect(toggle.test('<input type="checkbox" role="switch" />')).toBe(true);
|
||||
expect(toggle.test('{ role: "switch" }')).toBe(true);
|
||||
expect(toggle.test('<CheckboxField\n role="switch"\n label="Mit Details"\n/>')).toBe(false);
|
||||
expect(toggle.test('role === "switch"')).toBe(false);
|
||||
expect(checkbox.test('<span role={isTask ? "checkbox" : undefined}>')).toBe(true);
|
||||
expect(checkbox.test('<span role={isTask ? undefined : "checkbox"}>')).toBe(true);
|
||||
expect(checkbox.test('element.setAttribute("role", "checkbox")')).toBe(true);
|
||||
expect(checkbox.test("element.setAttribute('type','checkbox')")).toBe(true);
|
||||
expect(checkbox.test('{ role: on ? "checkbox" : "button" }')).toBe(true);
|
||||
expect(checkbox.test('role === "checkbox"')).toBe(false);
|
||||
expect(toggle.test('<button role={on ? "switch" : "button"}>')).toBe(true);
|
||||
expect(toggle.test('node.setAttribute("role", "switch")')).toBe(true);
|
||||
// The CheckboxField exemption ends with its own tag: a later element is caught again.
|
||||
expect(toggle.test('<CheckboxField label="A" />\n<input role="switch" />')).toBe(true);
|
||||
expect(checkbox.test('<CheckboxField label="A" /><span role="checkbox" />')).toBe(true);
|
||||
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);
|
||||
});
|
||||
|
||||
Reference in new issue
Block a user