diff --git a/src/components/cycle/__tests__/symptom-row.test.tsx b/src/components/cycle/__tests__/symptom-row.test.tsx
new file mode 100644
index 000000000..fb84de6a5
--- /dev/null
+++ b/src/components/cycle/__tests__/symptom-row.test.tsx
@@ -0,0 +1,75 @@
+/**
+ * Selecting a symptom must not move anything on the sheet (#1001 follow-up).
+ *
+ * The intensity selector used to appear inline beside a symptom chip in a
+ * wrapping row, so a selection widened the chip and pushed the chips after it
+ * along, sometimes out from under the finger reaching for the intensity. A
+ * symptom is now one row with its intensity beside it, and the intensity's
+ * space is laid out whether or not it is shown. What the test pins is exactly
+ * that: the selected and unselected row render the same boxes, the intensity
+ * group is present in both, and only its visibility and the colours change.
+ */
+import { describe, it, expect, vi } from "vitest";
+import { renderToStaticMarkup } from "react-dom/server";
+import { Droplet } from "lucide-react";
+
+vi.mock("../use-cycle", () => ({
+ useDeleteCustomSymptom: () => ({ mutateAsync: vi.fn(), isPending: false }),
+}));
+
+import { I18nProvider } from "@/lib/i18n/context";
+import { SymptomRow } from "../log-day-sheet";
+
+function render(active: boolean, severity: number | null = null): string {
+ return renderToStaticMarkup(
+
+ {}}
+ onSeverity={() => {}}
+ />
+ ,
+ );
+}
+
+/** The markup with every class and state attribute removed: the box tree. */
+function skeleton(html: string): string {
+ return html
+ .replace(/ class="[^"]*"/g, "")
+ .replace(/ (aria-[a-z]+|data-active|disabled|tabindex)(="[^"]*")?/g, "");
+}
+
+function severityGroup(html: string): string {
+ const m = /
]*data-slot="cycle-symptom-severity"[^>]*>/.exec(html);
+ if (!m) throw new Error("no severity group rendered");
+ return m[0];
+}
+
+describe("", () => {
+ it("lays out the same boxes whether or not the symptom is selected", () => {
+ expect(skeleton(render(true, 2))).toBe(skeleton(render(false)));
+ });
+
+ it("reserves the intensity's space while hidden, and reveals it in place", () => {
+ const idle = severityGroup(render(false));
+ const picked = severityGroup(render(true));
+ expect(idle).toContain("invisible");
+ expect(idle).toContain('aria-hidden="true"');
+ expect(picked).not.toContain("invisible");
+ expect(picked).not.toContain("aria-hidden");
+ });
+
+ it("keeps the intensity optional and out of reach until the symptom is picked", () => {
+ const idle = render(false);
+ // Four levels exist in both states, none of them focusable while hidden.
+ expect(idle.match(/aria-label="Severity \d of 4"/g)).toHaveLength(4);
+ expect(idle.match(/tabindex="-1"/g)).toHaveLength(4);
+ const picked = render(true);
+ expect(picked).not.toContain('tabindex="-1"');
+ expect(picked.match(/aria-pressed="true"/g)).toHaveLength(1);
+ });
+});
diff --git a/src/components/cycle/log-day-sheet.tsx b/src/components/cycle/log-day-sheet.tsx
index 9fad24d67..8d7c5411f 100644
--- a/src/components/cycle/log-day-sheet.tsx
+++ b/src/components/cycle/log-day-sheet.tsx
@@ -88,9 +88,9 @@ import type {
* detailed form posts a single day-log (flow, symptoms, BBT, OPK, mucus,
* intercourse + protection, free-text note). The write is optimistic-ish
* (the mutation invalidates the cycle keys on success so the calendar +
- * wheel repaint); the symptom chips reuse the mood-tag chip styling.
+ * wheel repaint); symptoms list one per line with their intensity beside them.
*
- * a11y: every chip is a real `
);
}
/**
- * The dashed ghost-chip in the symptom grid that opens a compact label + icon
- * popover to mint a new custom symptom. Matches the chip grid (same rounded-
- * full pill, same size) so it reads as part of the grid, not a foreign button.
+ * The dashed ghost row at the end of the custom symptoms that opens a compact
+ * label + icon popover to mint a new custom symptom. Same height and shape as
+ * a symptom row so it reads as part of the list, not a foreign button.
*/
function AddSymptomChip({ onCreated }: { onCreated: (key: string) => void }) {
const { t } = useTranslations();
@@ -1486,9 +1512,9 @@ function AddSymptomChip({ onCreated }: { onCreated: (key: string) => void }) {
-
+
{t("cycle.symptom.custom.add")}