From ef1abf79b7ee3bc61e41ee6188000af123507c4a Mon Sep 17 00:00:00 2001 From: Etienne Lescot Date: Wed, 5 Aug 2026 09:42:26 +0200 Subject: [PATCH] fix(notes): keep the note stylesheet in the production bundle MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `NotesWindow.module.css` was imported for its side effect only, with no binding. Rollup tree-shakes a binding-less CSS-module import out of the production bundle, so the shipped app carried zero `.tiptap` rules — dev looked fine because Vite injects module CSS at runtime there. Without `height: 100%` + `overflow-y: auto` the note body stops being a scroll container and grows to the height of its content. Paste a long note and ProseMirror scrolls the caret into view, which now scrolls the `overflow: hidden` shell instead: the toolbar leaves the viewport and the wheel cannot bring it back. Measured on a 400x540 window with ~5.8k characters pasted — editor 6360px tall, `scrollTop` pinned, toolbar at y=-5913, zero buttons hit-testable. Every selector in the file was already `:global`, so the CSS module bought nothing: rename it to plain `.css` and drop the wrappers. A plain CSS import is always emitted. This also restores the mirror transform, list markers, code blocks and heading sizes in production. --- ...NotesWindow.module.css => NotesWindow.css} | 66 ++++++++++--------- src/components/launch/NotesWindow.test.tsx | 27 ++++++++ src/components/launch/NotesWindow.tsx | 2 +- 3 files changed, 62 insertions(+), 33 deletions(-) rename src/components/launch/{NotesWindow.module.css => NotesWindow.css} (66%) diff --git a/src/components/launch/NotesWindow.module.css b/src/components/launch/NotesWindow.css similarity index 66% rename from src/components/launch/NotesWindow.module.css rename to src/components/launch/NotesWindow.css index b5793f25e..1b30f6b8b 100644 --- a/src/components/launch/NotesWindow.module.css +++ b/src/components/launch/NotesWindow.css @@ -1,5 +1,7 @@ -/* Tiptap sets class="tiptap" on the ProseMirror root — must be :global to escape CSS modules. */ -:global(.tiptap) { +/* Tiptap sets class="tiptap" on the ProseMirror root, so every selector here is global. + * Plain .css, NOT .module.css: this sheet is imported for its side effect only, and a + * binding-less CSS-module import is tree-shaken out of the production bundle. */ +.tiptap { height: 100%; width: 100%; outline: none; @@ -8,11 +10,11 @@ caret-color: #111827; } -:global(.notes-teleprompter-content[data-mirrored="true"]) { +.notes-teleprompter-content[data-mirrored="true"] { transform: scaleX(-1); } -:global(.tiptap) :first-child { +.tiptap :first-child { margin-top: 0; } @@ -21,38 +23,38 @@ * container. Every element below inherits that size, so `em` here equals the container size * and the default 16px rendering is unchanged. Headings are the exception — see below. */ -:global(.tiptap) p { +.tiptap p { min-height: 1.5em; line-height: 1.5; margin-top: 0.75em; margin-bottom: 0.75em; } -:global(.tiptap) p:first-child { +.tiptap p:first-child { margin-top: 0; } /* Tailwind preflight strips list markers — restore them for markdown-style input rules. */ -:global(.tiptap) ul, -:global(.tiptap) ol { +.tiptap ul, +.tiptap ol { padding-left: 1.5em; margin: 1em 0; } -:global(.tiptap) ul { +.tiptap ul { list-style-type: disc; } -:global(.tiptap) ol { +.tiptap ol { list-style-type: decimal; } -:global(.tiptap) li { +.tiptap li { display: list-item; } -:global(.tiptap) ul li p, -:global(.tiptap) ol li p { +.tiptap ul li p, +.tiptap ol li p { margin-top: 0.25em; margin-bottom: 0.25em; } @@ -62,42 +64,42 @@ * enlarged font size, not the container's, so switching them would change the default * rendering rather than just scale it. */ -:global(.tiptap) h1, -:global(.tiptap) h2, -:global(.tiptap) h3, -:global(.tiptap) h4, -:global(.tiptap) h5, -:global(.tiptap) h6 { +.tiptap h1, +.tiptap h2, +.tiptap h3, +.tiptap h4, +.tiptap h5, +.tiptap h6 { line-height: 1.1; margin-top: 2.5rem; text-wrap: pretty; } -:global(.tiptap) h1, -:global(.tiptap) h2 { +.tiptap h1, +.tiptap h2 { margin-top: 3.5rem; margin-bottom: 1.5rem; } -:global(.tiptap) h1 { +.tiptap h1 { font-size: 1.4em; } -:global(.tiptap) h2 { +.tiptap h2 { font-size: 1.2em; } -:global(.tiptap) h3 { +.tiptap h3 { font-size: 1.1em; } -:global(.tiptap) h4, -:global(.tiptap) h5, -:global(.tiptap) h6 { +.tiptap h4, +.tiptap h5, +.tiptap h6 { font-size: 1em; } -:global(.tiptap) code { +.tiptap code { background-color: #ede9fe; border-radius: 0.4rem; color: #111827; @@ -105,7 +107,7 @@ padding: 0.25em 0.3em; } -:global(.tiptap) pre { +.tiptap pre { background: #111827; border-radius: 0.5rem; color: #ffffff; @@ -114,20 +116,20 @@ padding: 0.75em 1em; } -:global(.tiptap) pre code { +.tiptap pre code { background: none; color: inherit; font-size: 0.8em; padding: 0; } -:global(.tiptap) blockquote { +.tiptap blockquote { border-left: 3px solid #d1d5db; margin: 1.5em 0; padding-left: 1em; } -:global(.tiptap) hr { +.tiptap hr { border: none; border-top: 1px solid #e5e7eb; margin: 2em 0; diff --git a/src/components/launch/NotesWindow.test.tsx b/src/components/launch/NotesWindow.test.tsx index cb77abcb1..d3f212078 100644 --- a/src/components/launch/NotesWindow.test.tsx +++ b/src/components/launch/NotesWindow.test.tsx @@ -1,4 +1,6 @@ import "@testing-library/jest-dom"; +import { readFileSync } from "node:fs"; +import { resolve } from "node:path"; import { act, render, screen, waitFor } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import type { Editor } from "@tiptap/react"; @@ -395,3 +397,28 @@ describe("NotesWindow teleprompter mode", () => { expect(localStorage.getItem("notes")).toBe("

Updated

"); }); }); + +describe("NotesWindow stylesheet", () => { + // The note body only scrolls because `.tiptap` carries `height: 100%` + `overflow-y: auto`. + // Those rules reach the app through a side-effect import, and a side-effect import of a + // *CSS module* is tree-shaken out of the production bundle — dev looked fine while the + // packaged app let a long note grow past its slot, scroll the whole shell out of view and + // take the toolbar with it. A plain `.css` import is always emitted. + const read = (file: string) => + readFileSync(resolve(process.cwd(), "src/components/launch", file), "utf8"); + + it("is imported as plain CSS, never as a CSS module", () => { + const source = read("NotesWindow.tsx"); + + expect(source).toContain('import "./NotesWindow.css"'); + expect(source).not.toContain(".module.css"); + }); + + it("keeps the note body a scroll container", () => { + const css = read("NotesWindow.css"); + const body = css.match(/\.tiptap\s*\{[^}]*\}/)?.[0] ?? ""; + + expect(body).toMatch(/height:\s*100%/); + expect(body).toMatch(/overflow-y:\s*auto/); + }); +}); diff --git a/src/components/launch/NotesWindow.tsx b/src/components/launch/NotesWindow.tsx index cf5819ae9..7e8031dad 100644 --- a/src/components/launch/NotesWindow.tsx +++ b/src/components/launch/NotesWindow.tsx @@ -17,7 +17,7 @@ import { saveNotesTeleprompterSettings, TELEPROMPTER_SPEED_STEP, } from "./notesTeleprompter"; -import "./NotesWindow.module.css"; +import "./NotesWindow.css"; export function NotesWindow() { const [settings, setSettings] = useState(loadNotesTeleprompterSettings);