From bc1633db4009620235f560eed4fd0feb48c18bcd Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?=
Date: Sat, 25 Jul 2026 21:59:47 +0200
Subject: [PATCH] fix(export): make the standard report a click, not a
preselection
The scope picker arrived with the standard doctor's report already ticked,
so someone who opened the panel and pressed Generate sent material they
never chose. Nothing is selected now until a person selects it.
The template keeps its definition and loses its automatic application: it
sits on a button at the top of the picker, named, on both surfaces that
mount the picker, and it stays there so the standard set can be restored
after experimenting. One click still gets a complete doctor's report.
Generate and the share-link create refuse an empty scope, with the scope
line beside them saying what to do rather than reporting a failure after
the press; the line also describes the control, so the disabled state is
not silent for a screen reader.
---
e2e/settings-export.spec.ts | 66 ++++++++++--
e2e/share-documents.spec.ts | 11 +-
messages/de.json | 8 +-
messages/en.json | 8 +-
messages/es.json | 8 +-
messages/fr.json | 8 +-
messages/it.json | 8 +-
messages/pl.json | 8 +-
.../__tests__/report-selection-panel.test.tsx | 101 ++++++++++++------
.../__tests__/share-link-create-form.test.tsx | 37 +++++++
.../__tests__/sharing-reselect.test.tsx | 5 +-
.../report-selection/report-scope-picker.tsx | 41 +++++++
.../report-selection/scope-summary.tsx | 10 ++
.../report-selection/selection-panel.tsx | 53 +++++----
.../settings/share-link-create-form.tsx | 31 +++---
.../__tests__/catalogue-guard.test.ts | 36 ++++++-
src/lib/report-selection/template.ts | 30 ++++--
17 files changed, 353 insertions(+), 116 deletions(-)
diff --git a/e2e/settings-export.spec.ts b/e2e/settings-export.spec.ts
index dbe14a4d8..a6ef1db5c 100644
--- a/e2e/settings-export.spec.ts
+++ b/e2e/settings-export.spec.ts
@@ -101,12 +101,11 @@ test.describe("Settings → Export & Import", () => {
test("opens the scope picker expanded on a first run, fenced tier and all", async ({
page,
}) => {
- // The three panel states are the release's whole point, and the first one
- // is the only place a template is applied. It has to be VISIBLE — a
- // template behind a collapsed disclosure is opt-out wearing a hat — so the
- // picker, its twelve group rows and the fenced tier all render without a
- // click. Asserted against the export mount; the share form runs its own
- // picker with its own defaults.
+ // The three panel states are the release's whole point, and on the first
+ // one nothing is selected at all. The picker still opens without a click,
+ // because with an empty scope the standard-report button and the named
+ // groups are exactly what the person needs in front of them. Asserted
+ // against the export mount; the share form runs its own picker.
await openGesundheitsakte(page);
await expect(page.getByTestId("report-scope-picker-export")).toBeVisible({
timeout: 10_000,
@@ -128,6 +127,61 @@ test.describe("Settings → Export & Import", () => {
await expect(page.getByTestId("report-scope-summary-export")).toBeVisible();
});
+ test("first run selects nothing and refuses to generate until it does", async ({
+ page,
+ }) => {
+ // The rule the whole surface exists for: nothing reaches the report that
+ // was not ticked. So a first run starts empty, Generate refuses the press
+ // and says why, and the one-click action is what makes that cost nothing —
+ // it fills the standard set and the control comes alive.
+ await openGesundheitsakte(page);
+ const generate = page.getByTestId("health-record-generate");
+ await expect(generate).toBeVisible({ timeout: 10_000 });
+ await expect(generate).toBeDisabled();
+ await expect(
+ page.getByTestId("report-group-row-vitals-export"),
+ ).toContainText("0/7");
+ await expect(page.getByTestId("report-scope-summary-export")).toContainText(
+ "Nothing selected yet",
+ );
+
+ await page.getByTestId("report-apply-standard-export").click();
+
+ await expect(generate).toBeEnabled();
+ // Exactly the shipped set: the three classic vitals, weight and BMI, and
+ // nothing at all from the groups the template leaves alone.
+ await expect(
+ page.getByTestId("report-group-row-vitals-export"),
+ ).toContainText("3/7");
+ await expect(
+ page.getByTestId("report-group-row-body-export"),
+ ).toContainText("2/12");
+ await expect(
+ page.getByTestId("report-group-row-cardio-export"),
+ ).toContainText("0/14");
+ // The fenced tier is not part of any one-click set.
+ await expect(
+ page.getByTestId("report-scope-summary-export"),
+ ).not.toContainText("incl.");
+ });
+
+ test("a share link cannot be minted on an empty scope either", async ({
+ page,
+ }) => {
+ await openGesundheitsakte(page);
+ const create = page.getByTestId("share-create-submit");
+ await expect(create).toBeVisible({ timeout: 10_000 });
+ await page.locator("#share-label").fill("Cardiology");
+ // A label alone is not a scope: the link would serve a page with nothing
+ // on it, so the control stays shut until something is chosen.
+ await expect(create).toBeDisabled();
+ await expect(page.getByTestId("report-scope-summary-share")).toContainText(
+ "Nothing selected yet",
+ );
+ await page.getByTestId("report-apply-standard-share").click();
+ await expect(create).toBeEnabled();
+ });
+
test("clears the tap floor on the group rows at 390 px", async ({ page }) => {
await page.setViewportSize({ width: 390, height: 844 });
await openGesundheitsakte(page);
diff --git a/e2e/share-documents.spec.ts b/e2e/share-documents.spec.ts
index 28d00a7d6..862f4a426 100644
--- a/e2e/share-documents.spec.ts
+++ b/e2e/share-documents.spec.ts
@@ -51,9 +51,10 @@ interface CreatedShare {
}
/**
- * Drive the owner create flow: open the picker, isolate the share-fixture trio
- * by search, select all three, create the link, and read the one-time token +
- * passphrase off the reveal card. Also asserts the QR promotion.
+ * Drive the owner create flow: name the link, choose a record scope, open the
+ * picker, isolate the share-fixture trio by search, select all three, create
+ * the link, and read the one-time token + passphrase off the reveal card. Also
+ * asserts the QR promotion.
*/
async function createShareWithDocs(page: Page): Promise {
const label = `${SHARE_DOC_PREFIX} ${Date.now()}`;
@@ -67,6 +68,10 @@ async function createShareWithDocs(page: Page): Promise {
await page.goto("/settings/sharing");
await sharingDataReady;
await page.locator("#share-label").fill(label);
+ // A record link starts with an empty scope and cannot be minted on one, so
+ // the scope is chosen here the way an owner chooses it: one click on the
+ // named standard set.
+ await page.getByTestId("report-apply-standard-share").click();
// Open the document picker and isolate the seeded trio.
await page.getByTestId("share-attach-open").click();
diff --git a/messages/de.json b/messages/de.json
index 334d94127..4728f85d6 100644
--- a/messages/de.json
+++ b/messages/de.json
@@ -5317,8 +5317,7 @@
"rangeCustomEnd": "Bis",
"errorGeneric": "Der Export konnte nicht erstellt werden. Bitte erneut versuchen.",
"errorRateLimit": "Das Exportlimit für diese Stunde ist erreicht.",
- "errorModuleDisabled": "Der Gesundheitsakten-Export ist für dieses Konto ausgeschaltet.",
- "errorEmptySelection": "Wählen Sie mindestens einen Eintrag aus."
+ "errorModuleDisabled": "Der Gesundheitsakten-Export ist für dieses Konto ausgeschaltet."
},
"globalExcludedInjectionSitesLabel": "Global ausgeschlossene Injektionsstellen",
"globalExcludedInjectionSitesHint": "Hier gelistete Stellen werden für keine Medikation angeboten und beim Erfassen einer Dosis abgelehnt — auch wenn eine Medikation sie bevorzugt.",
@@ -8888,11 +8887,12 @@
"leafMood": "Stimmung",
"leafCycle": "Menstruationszyklus",
"templateStandard": "Standard-Arztbericht",
- "templateApplied": "{template} ist vorausgewählt. Prüfen Sie ihn vor dem Erstellen.",
+ "applyStandard": "{template} übernehmen",
+ "standardHint": "Ein Klick füllt aus, was ein normaler Arztbesuch meist braucht. Jeder Eintrag bleibt änderbar.",
"groupCount": "{on}/{total}",
"groupDisclosure": "Einträge in {group} anzeigen",
"sensitiveHint": "Hier ist nie etwas vorausgewählt. Wählen Sie jeden Eintrag selbst.",
- "scopeNone": "Nichts ausgewählt — das Dokument wäre leer.",
+ "scopeNone": "Noch nichts ausgewählt. Wählen Sie aus, was enthalten sein soll, oder übernehmen Sie den Standardbericht.",
"scopeCount": "{count} Einträge aus {groups} Bereichen",
"scopeSensitive": "inkl. {names}"
}
diff --git a/messages/en.json b/messages/en.json
index 564f393b2..4191253be 100644
--- a/messages/en.json
+++ b/messages/en.json
@@ -5317,8 +5317,7 @@
"rangeCustomEnd": "To",
"errorGeneric": "The export could not be generated. Please try again.",
"errorRateLimit": "You have reached the export limit for this hour.",
- "errorModuleDisabled": "The health record export is switched off for this account.",
- "errorEmptySelection": "Pick at least one entry to include."
+ "errorModuleDisabled": "The health record export is switched off for this account."
},
"globalExcludedInjectionSitesLabel": "Globally excluded injection sites",
"globalExcludedInjectionSitesHint": "Sites listed here are never offered for any medication and rejected when logging a dose, even if a medication prefers them.",
@@ -8888,11 +8887,12 @@
"leafMood": "Mood",
"leafCycle": "Menstrual cycle",
"templateStandard": "Standard doctor's report",
- "templateApplied": "{template} is preselected. Check it before you generate.",
+ "applyStandard": "Apply {template}",
+ "standardHint": "One click fills in what a general visit usually needs. Every entry stays editable.",
"groupCount": "{on}/{total}",
"groupDisclosure": "Show the entries in {group}",
"sensitiveHint": "Nothing here is ever preselected. Pick each entry yourself.",
- "scopeNone": "Nothing selected — the document would be empty.",
+ "scopeNone": "Nothing selected yet. Choose what to include, or apply the standard report.",
"scopeCount": "{count} entries from {groups} areas",
"scopeSensitive": "incl. {names}"
}
diff --git a/messages/es.json b/messages/es.json
index 7f14c9594..d046fbd97 100644
--- a/messages/es.json
+++ b/messages/es.json
@@ -5317,8 +5317,7 @@
"rangeCustomEnd": "Hasta",
"errorGeneric": "No se pudo generar la exportación. Inténtelo de nuevo.",
"errorRateLimit": "Ha alcanzado el límite de exportaciones de esta hora.",
- "errorModuleDisabled": "La exportación del expediente está desactivada para esta cuenta.",
- "errorEmptySelection": "Seleccione al menos una entrada."
+ "errorModuleDisabled": "La exportación del expediente está desactivada para esta cuenta."
},
"globalExcludedInjectionSitesLabel": "Sitios de inyección excluidos globalmente",
"globalExcludedInjectionSitesHint": "Los sitios listados aquí no se ofrecen para ningún medicamento y se rechazan al registrar una dosis, aunque un medicamento los prefiera.",
@@ -8888,11 +8887,12 @@
"leafMood": "Estado de ánimo",
"leafCycle": "Ciclo menstrual",
"templateStandard": "Informe médico estándar",
- "templateApplied": "{template} está preseleccionado. Revíselo antes de generar.",
+ "applyStandard": "Aplicar {template}",
+ "standardHint": "Un clic rellena lo que suele necesitar una consulta general. Cada entrada sigue siendo editable.",
"groupCount": "{on}/{total}",
"groupDisclosure": "Mostrar las entradas de {group}",
"sensitiveHint": "Aquí nunca hay nada preseleccionado. Elija cada entrada usted mismo.",
- "scopeNone": "Nada seleccionado: el documento estaría vacío.",
+ "scopeNone": "Nada seleccionado todavía. Elija qué incluir o aplique el informe estándar.",
"scopeCount": "{count} entradas de {groups} áreas",
"scopeSensitive": "incl. {names}"
}
diff --git a/messages/fr.json b/messages/fr.json
index 8aa90fa95..2eda57ea9 100644
--- a/messages/fr.json
+++ b/messages/fr.json
@@ -5317,8 +5317,7 @@
"rangeCustomEnd": "Au",
"errorGeneric": "L'export n'a pas pu être généré. Veuillez réessayer.",
"errorRateLimit": "Vous avez atteint la limite d'exports pour cette heure.",
- "errorModuleDisabled": "L'export du dossier est désactivé pour ce compte.",
- "errorEmptySelection": "Sélectionnez au moins un élément."
+ "errorModuleDisabled": "L'export du dossier est désactivé pour ce compte."
},
"globalExcludedInjectionSitesLabel": "Sites d'injection exclus globalement",
"globalExcludedInjectionSitesHint": "Les sites listés ici ne sont proposés pour aucun médicament et sont refusés lors de l'enregistrement d'une dose, même si un médicament les privilégie.",
@@ -8888,11 +8887,12 @@
"leafMood": "Humeur",
"leafCycle": "Cycle menstruel",
"templateStandard": "Compte rendu médical standard",
- "templateApplied": "{template} est présélectionné. Vérifiez-le avant de générer.",
+ "applyStandard": "Appliquer {template}",
+ "standardHint": "Un clic remplit ce dont une consultation générale a généralement besoin. Chaque élément reste modifiable.",
"groupCount": "{on}/{total}",
"groupDisclosure": "Afficher les éléments de {group}",
"sensitiveHint": "Rien n'est jamais présélectionné ici. Choisissez chaque élément vous-même.",
- "scopeNone": "Rien de sélectionné — le document serait vide.",
+ "scopeNone": "Rien de sélectionné pour l'instant. Choisissez ce qui doit figurer, ou appliquez le compte rendu standard.",
"scopeCount": "{count} éléments issus de {groups} domaines",
"scopeSensitive": "dont {names}"
}
diff --git a/messages/it.json b/messages/it.json
index 198f067a2..f772e54ed 100644
--- a/messages/it.json
+++ b/messages/it.json
@@ -5317,8 +5317,7 @@
"rangeCustomEnd": "Al",
"errorGeneric": "Non è stato possibile generare l'esportazione. Riprova.",
"errorRateLimit": "Hai raggiunto il limite di esportazioni per questa ora.",
- "errorModuleDisabled": "L'esportazione della cartella è disattivata per questo account.",
- "errorEmptySelection": "Seleziona almeno una voce."
+ "errorModuleDisabled": "L'esportazione della cartella è disattivata per questo account."
},
"globalExcludedInjectionSitesLabel": "Siti di iniezione esclusi globalmente",
"globalExcludedInjectionSitesHint": "I siti elencati qui non vengono proposti per alcun farmaco e vengono rifiutati al momento della registrazione di una dose, anche se un farmaco li preferisce.",
@@ -8888,11 +8887,12 @@
"leafMood": "Umore",
"leafCycle": "Ciclo mestruale",
"templateStandard": "Referto medico standard",
- "templateApplied": "{template} è preselezionato. Controllalo prima di generare.",
+ "applyStandard": "Applica {template}",
+ "standardHint": "Un clic compila ciò che serve di solito a una visita generale. Ogni voce resta modificabile.",
"groupCount": "{on}/{total}",
"groupDisclosure": "Mostra le voci di {group}",
"sensitiveHint": "Qui non è mai preselezionato nulla. Scegli ogni voce tu stesso.",
- "scopeNone": "Niente selezionato: il documento sarebbe vuoto.",
+ "scopeNone": "Ancora niente selezionato. Scegli cosa includere oppure applica il referto standard.",
"scopeCount": "{count} voci da {groups} aree",
"scopeSensitive": "incl. {names}"
}
diff --git a/messages/pl.json b/messages/pl.json
index 563f4fc1f..45354f0dc 100644
--- a/messages/pl.json
+++ b/messages/pl.json
@@ -5317,8 +5317,7 @@
"rangeCustomEnd": "Do",
"errorGeneric": "Nie udało się wygenerować eksportu. Spróbuj ponownie.",
"errorRateLimit": "Osiągnięto limit eksportów w tej godzinie.",
- "errorModuleDisabled": "Eksport dokumentacji jest wyłączony dla tego konta.",
- "errorEmptySelection": "Wybierz co najmniej jedną pozycję."
+ "errorModuleDisabled": "Eksport dokumentacji jest wyłączony dla tego konta."
},
"globalExcludedInjectionSitesLabel": "Globalnie wykluczone miejsca iniekcji",
"globalExcludedInjectionSitesHint": "Miejsca wymienione tutaj nie są proponowane dla żadnego leku i są odrzucane przy rejestrowaniu dawki, nawet jeśli lek je preferuje.",
@@ -8888,11 +8887,12 @@
"leafMood": "Nastrój",
"leafCycle": "Cykl menstruacyjny",
"templateStandard": "Standardowy raport lekarski",
- "templateApplied": "{template} jest wstępnie wybrany. Sprawdź go przed wygenerowaniem.",
+ "applyStandard": "Zastosuj {template}",
+ "standardHint": "Jedno kliknięcie wypełnia to, czego zwykle wymaga wizyta u lekarza rodzinnego. Każdą pozycję można potem zmienić.",
"groupCount": "{on}/{total}",
"groupDisclosure": "Pokaż pozycje w {group}",
"sensitiveHint": "Nic tu nie jest wstępnie wybrane. Wybierz każdą pozycję samodzielnie.",
- "scopeNone": "Nic nie wybrano — dokument byłby pusty.",
+ "scopeNone": "Nic jeszcze nie wybrano. Wybierz, co ma się znaleźć w dokumencie, albo zastosuj raport standardowy.",
"scopeCount": "{count} pozycji z {groups} obszarów",
"scopeSensitive": "w tym {names}"
}
diff --git a/src/components/settings/__tests__/report-selection-panel.test.tsx b/src/components/settings/__tests__/report-selection-panel.test.tsx
index 3484046ff..7d0a34249 100644
--- a/src/components/settings/__tests__/report-selection-panel.test.tsx
+++ b/src/components/settings/__tests__/report-selection-panel.test.tsx
@@ -7,22 +7,26 @@
* stick, and it is gated on the user id so a late re-resolve never clobbers
* edits in flight.
*
- * The three panel states are the load-bearing part. First run must open the
- * picker EXPANDED with the named template applied and the fenced tier visibly
- * empty, so the user presses Generate having seen twelve named groups; a
- * repeat run must open collapsed carrying the owner's own last scope. A
- * template silently applied behind a collapsed disclosure would be opt-out
- * wearing a hat.
+ * The three panel states are the load-bearing part. A first run must select
+ * NOTHING — no template, no server default, no group half on — and must leave
+ * Generate disabled until a person ticks something, with the standard-report
+ * button one click away. A repeat run must open collapsed carrying the owner's
+ * own last scope. A set that arrives without being asked for is a
+ * preselection whatever it is called, and material nobody ticked is exactly
+ * what this surface must never send.
*
* SSR-only, per the project convention for this surface.
*
- * Mutation checks:
+ * Mutation checks (each verified red, then reverted):
* - remove the seeding block → "pre-fills the input" and "restores the saved
* selection" go red.
+ * - seed `selected` from `STANDARD_TEMPLATE_LEAVES` → "selects nothing on a
+ * first run", "counts nothing in any group" and "disables Generate" go
+ * red.
* - start `pickerOpen` at `false` → "opens expanded on the first run" goes
* red.
- * - add a fenced leaf to `STANDARD_TEMPLATE_LEAVES` → "checks no fenced leaf
- * on the first run" goes red.
+ * - drop `selected.size === 0` from the Generate `disabled` expression →
+ * "disables Generate while the scope is empty" goes red.
*/
import { describe, expect, it, vi } from "vitest";
import { renderToStaticMarkup } from "react-dom/server";
@@ -126,6 +130,15 @@ function practiceInputValue(html: string): string | null {
return input.match(/value="([^"]*)"/)?.[1] ?? "";
}
+/** Whether the Generate control refuses the press. */
+function generateDisabled(html: string): boolean {
+ const button = html.match(
+ /
)}
+ {/* A record link with an empty scope cannot be minted: it would serve
+ a page with nothing on it. The scope line above says so and says
+ what to do about it, and describes the button so the disabled state
+ is not silent for a screen reader. */}
{createMutation.isPending && (
diff --git a/src/lib/report-selection/__tests__/catalogue-guard.test.ts b/src/lib/report-selection/__tests__/catalogue-guard.test.ts
index 8a36daf46..dc491a744 100644
--- a/src/lib/report-selection/__tests__/catalogue-guard.test.ts
+++ b/src/lib/report-selection/__tests__/catalogue-guard.test.ts
@@ -29,7 +29,7 @@ import {
STRUCTURED_LEAF_IDS,
leafLabelKey,
} from "../catalogue";
-import { STANDARD_TEMPLATE_LEAVES } from "../template";
+import { STANDARD_TEMPLATE_LEAVES, standardTemplateFor } from "../template";
const EN = JSON.parse(
readFileSync(
@@ -112,3 +112,37 @@ describe("template purity", () => {
);
});
});
+
+/**
+ * The set behind the one-click action, checked as a set.
+ *
+ * The button is the only thing that applies the template now, so what it
+ * applies has to be exactly the shipped list and nothing beside it — a leaf
+ * that crept in here would land in a document the person believes they chose
+ * from a named list.
+ *
+ * Mutation check (verified red, then reverted): make `standardTemplateFor`
+ * ignore its argument → "drops only what the surface hides" goes red.
+ */
+describe("the standard-report action", () => {
+ it("applies exactly the shipped set and nothing outside it", () => {
+ expect(standardTemplateFor()).toEqual([...STANDARD_TEMPLATE_LEAVES]);
+ const outside = standardTemplateFor().filter(
+ (leaf) => !STANDARD_TEMPLATE_LEAVES.includes(leaf),
+ );
+ expect(outside).toEqual([]);
+ // Nothing fenced, on any surface: the fence is not a matter of which
+ // button you press.
+ expect(
+ standardTemplateFor().filter((leaf) => SENSITIVE_LEAF_IDS.includes(leaf)),
+ ).toEqual([]);
+ });
+
+ it("drops only what the surface hides", () => {
+ const onShare = standardTemplateFor(["INSURANCE"]);
+ expect(onShare).not.toContain("INSURANCE");
+ expect(onShare).toEqual(
+ STANDARD_TEMPLATE_LEAVES.filter((leaf) => leaf !== "INSURANCE"),
+ );
+ });
+});
diff --git a/src/lib/report-selection/template.ts b/src/lib/report-selection/template.ts
index b83811681..347b4e051 100644
--- a/src/lib/report-selection/template.ts
+++ b/src/lib/report-selection/template.ts
@@ -1,14 +1,15 @@
/**
* The one shipped template: what a general doctor's visit usually needs.
*
- * It is applied on the FIRST run only, with the panel open, so the user
- * presses Generate having seen twelve named groups and an empty fenced tier.
- * That is consent to a named bundle, not a server-chosen default — nothing
- * here is applied to a stored selection, to a share link, or to any surface
- * that cannot ask a human.
+ * Nothing applies it on its own. It reaches a selection only when a person
+ * presses the button that carries its name, on a panel they are looking at —
+ * never on load, never on a stored selection, never on a share link, never on
+ * a surface that cannot ask a human. A set that arrives without being asked
+ * for is a preselection whatever it is called, and the one thing this release
+ * refuses is material in a report nobody ticked.
*
* The template intersects the fenced tier at zero leaves, and
- * `report-template-purity.test.ts` fails the build if that stops being true.
+ * `catalogue-guard.test.ts` fails the build if that stops being true.
*/
import { SENSITIVE_LEAF_IDS, type ReportLeafId } from "./catalogue";
@@ -39,8 +40,23 @@ export const STANDARD_TEMPLATE_LEAVES: readonly ReportLeafId[] = [
"ILLNESS_EPISODES",
];
-/** The template's i18n label — it is named in the panel, never anonymous. */
+/** The template's i18n label — it is named on the button, never anonymous. */
export const STANDARD_TEMPLATE_LABEL_KEY = "reportSelection.templateStandard";
+/**
+ * The template as one surface can offer it: the shipped set minus the leaves
+ * that surface may not carry (the share-link form hides the insurance leaf,
+ * which the server refuses anyway).
+ *
+ * Pure, so "one click applies exactly the standard set and nothing outside it"
+ * is provable without a DOM.
+ */
+export function standardTemplateFor(
+ hiddenLeaves: readonly ReportLeafId[] = [],
+): ReportLeafId[] {
+ const hidden = new Set(hiddenLeaves);
+ return STANDARD_TEMPLATE_LEAVES.filter((leaf) => !hidden.has(leaf));
+}
+
/** Exported so the purity guard reads the same set the panel does. */
export const SENSITIVE_LEAVES = SENSITIVE_LEAF_IDS;