mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-10 13:22:14 +00:00
The running status line now carries its seconds right after the text, in the line's own muted tone, instead of at the far edge of the column. The live count and every finished round read "4 s", the same spacing as the settled summary, with a no-break space so a round never wraps between number and unit. Reply pills (follow-ups, clarifying choices, memory and plan decisions) sit directly under the answer, and the action row is the last thing in the column. Beside a pointer the row stays invisible until hover or focus; above the pills it left an empty band between an answer and its replies. Hovering or focusing the message still reveals the row without moving anything. Keyboard focus in the Coach panel is a solid ring in the neutral text-field focus tone, never the purple ring: the pills, Undo and Change merge it over the button primitive, and the hand-built controls carry it directly. A structural test keeps the purple ring out of the panel. A disabled native select now looks disabled, like Input and Select, so a thinking depth locked by the operator no longer reads as a live choice. In Settings, thinking depth takes the shape of Tone and Reply length beside it; a memory entry's Forget is an icon like its edit; the operator's highest-depth label sits on the switches' left edge.
57 lines
2.1 KiB
TypeScript
57 lines
2.1 KiB
TypeScript
/**
|
|
* Structural guard on keyboard focus in the Coach panel.
|
|
*
|
|
* The purple `--ring` is gone from every Coach control: a tap draws nothing,
|
|
* keyboard focus draws a solid ring in the neutral text-field focus tone
|
|
* (`--input-focus`). Two checks:
|
|
*
|
|
* 1. No class string under `coach-panel/` names the purple ring, border or
|
|
* outline on focus. The sweep must find a floor of neutral focus rings,
|
|
* so a matcher that stops matching fails instead of passing on nothing.
|
|
* 2. The controls built on the `Button` primitive inside an answer (the
|
|
* reply pills, Undo, Change) pass `COACH_FOCUS_RING`, which replaces the
|
|
* primitive's purple ring when the classes merge.
|
|
*/
|
|
import { readdirSync, readFileSync } from "node:fs";
|
|
import { join } from "node:path";
|
|
|
|
import { describe, expect, it } from "vitest";
|
|
|
|
const DIR = join(__dirname, "..", "components", "insights", "coach-panel");
|
|
|
|
const SOURCES = readdirSync(DIR)
|
|
.filter((name) => /\.(tsx|ts)$/.test(name))
|
|
.map((name) => ({ name, text: readFileSync(join(DIR, name), "utf8") }));
|
|
|
|
const PURPLE_FOCUS =
|
|
/(?:focus|focus-visible|focus-within):(?:ring-ring|border-ring|outline-ring|ring-primary|border-primary)\b/g;
|
|
|
|
describe("Coach focus ring", () => {
|
|
it("never names the purple ring on focus", () => {
|
|
const offenders = SOURCES.flatMap(({ name, text }) =>
|
|
[...text.matchAll(PURPLE_FOCUS)].map((m) => `${name}: ${m[0]}`),
|
|
);
|
|
expect(offenders).toEqual([]);
|
|
});
|
|
|
|
it("finds the neutral ring where it expects it", () => {
|
|
const neutral = SOURCES.reduce(
|
|
(n, { text }) =>
|
|
n + (text.match(/focus-visible:ring-input-focus\b/g)?.length ?? 0),
|
|
0,
|
|
);
|
|
expect(neutral).toBeGreaterThanOrEqual(15);
|
|
});
|
|
|
|
it.each(["suggested-replies.tsx", "memory-note.tsx", "assumption-line.tsx"])(
|
|
"%s merges the neutral ring over the Button primitive",
|
|
(file) => {
|
|
const text = SOURCES.find((s) => s.name === file)?.text ?? "";
|
|
expect(text).toMatch(
|
|
/import \{ COACH_FOCUS_RING \} from "\.\/focus-ring"/,
|
|
);
|
|
expect(text).toMatch(/cn\([\s\S]*?COACH_FOCUS_RING,?\s*\)/);
|
|
},
|
|
);
|
|
});
|