Skip to content

Commit 801611a

Browse files
Junyi-99claude
andauthored
chore: slim bundle, drop Faro, isolate CSS from Overleaf (#168)
## Summary - **Slim code highlighting** — replace `@streamdown/code` (bundles all 50+ Shiki grammars) with a custom plugin loading only 13 languages + 2 themes. `paperdebugger.js` 17 MB → 9.4 MB. - **Remove Grafana Faro** observability SDK from the extension bundle and build pipeline. - **Isolate extension CSS from Overleaf** — scope all CSS under a `.pd-scope` class (via `postcss-prefix-selector`, content-script build only) so Tailwind preflight + heroui no longer leak into and mutate the Overleaf page. All UI surfaces (floating window, embed sidebar, heroui popovers via a dedicated `#pd-portal`, toolbar buttons) are self-scoped. - **Telemetry** — LLM TTFT, JS heap gauge, event capture. ## Testing notes Automated: tsc + build clean. Manual (needs a browser on Overleaf): - Overleaf's own styling no longer altered - All 5 display modes render styled (floating, right-fixed, bottom-fixed, fullscreen, embed) - Dark mode in floating + embed - heroui modals/tooltips/selects styled - Toolbar button + "Add to Chat" styled 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01ECe2qZWextwVCycC9EveFe --------- Co-authored-by: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
1 parent 968a7ca commit 801611a

20 files changed

Lines changed: 4832 additions & 1464 deletions

webapp/_webapp/package-lock.json

Lines changed: 4467 additions & 1348 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

webapp/_webapp/package.json

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,21 @@
2525
"@buf/googleapis_googleapis.bufbuild_es": "^2.2.3-20250211200939-546238c53f73.1",
2626
"@bufbuild/protobuf": "^2.5.1",
2727
"@capacitor-community/apple-sign-in": "^7.0.1",
28-
"@grafana/faro-web-sdk": "^2.0.2",
29-
"@grafana/faro-web-tracing": "^2.0.2",
3028
"@heroui/react": "^2.7.9",
3129
"@iconify/react": "^6.0.0",
3230
"@lukemorales/query-key-factory": "^1.3.4",
31+
"@opentelemetry/exporter-logs-otlp-http": "^0.219.0",
32+
"@opentelemetry/exporter-metrics-otlp-http": "^0.219.0",
33+
"@opentelemetry/exporter-metrics-otlp-proto": "^0.219.0",
34+
"@opentelemetry/exporter-trace-otlp-http": "^0.219.0",
35+
"@opentelemetry/instrumentation": "^0.219.0",
36+
"@opentelemetry/instrumentation-document-load": "^0.64.0",
37+
"@opentelemetry/instrumentation-fetch": "^0.219.0",
38+
"@opentelemetry/resources": "^2.8.0",
39+
"@opentelemetry/sdk-logs": "^0.219.0",
40+
"@opentelemetry/sdk-metrics": "^2.8.0",
41+
"@opentelemetry/sdk-trace-web": "^2.8.0",
42+
"@opentelemetry/semantic-conventions": "^1.41.1",
3343
"@r2wc/react-to-web-component": "^2.1.0",
3444
"@streamdown/cjk": "^1.0.1",
3545
"@streamdown/code": "^1.0.1",
@@ -56,13 +66,13 @@
5666
"semver": "^7.7.2",
5767
"streamdown": "^2.1.0",
5868
"uuid": "^11.1.0",
69+
"web-vitals": "^5.3.0",
5970
"zustand": "^5.0.5"
6071
},
6172
"devDependencies": {
6273
"@codemirror/state": "^6.5.2",
6374
"@codemirror/view": "^6.37.1",
6475
"@eslint/js": "^9.28.0",
65-
"@grafana/faro-rollup-plugin": "^0.7.0",
6676
"@types/bun": "^1.3.5",
6777
"@types/chrome": "^0.0.326",
6878
"@types/codemirror": "^5.60.16",
@@ -78,6 +88,7 @@
7888
"eslint-plugin-react-refresh": "^0.4.20",
7989
"globals": "^16.2.0",
8090
"nodemon": "^3.1.10",
91+
"postcss-prefix-selector": "^2.1.1",
8192
"prettier": "3.5.3",
8293
"tailwindcss": "^3.4.17",
8394
"typescript": "~5.8.3",

webapp/_webapp/postcss.config.js

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,38 @@
1+
// ponytail: scope ALL css under .pd-scope ONLY for the content-script build
2+
// (VITE_CONFIG=default) so preflight/heroui don't leak into Overleaf. The
3+
// .pd-scope class sits on every container we mount into (#paper-debugger-root
4+
// and #pd-embed-sidebar). Settings/popup are standalone pages and stay unscoped.
5+
const SCOPE = ".pd-scope";
6+
const scoped = process.env.VITE_CONFIG === "default";
7+
18
export default {
29
plugins: {
310
tailwindcss: {},
11+
...(scoped && {
12+
"postcss-prefix-selector": {
13+
prefix: SCOPE,
14+
transform(prefix, selector, prefixedSelector) {
15+
// Root-level globals apply to the scope container itself.
16+
if (selector === "html" || selector === "body" || selector === ":root") return prefix;
17+
// box-sizing/* reset: cover the scope element and everything inside it.
18+
if (selector === "*") return `${prefix}, ${prefix} *`;
19+
// Theme toggles + :root variants live ON the scope element → compound, not descendant.
20+
if (/^\.(dark|light)\b/.test(selector)) return prefix + selector;
21+
if (selector.startsWith(":root")) return prefix + selector.slice(":root".length);
22+
if (selector.startsWith("[data-theme")) return prefix + selector;
23+
// These elements ARE scope roots (carry .pd-scope on themselves) → compound,
24+
// so rules targeting the element itself keep matching after scoping.
25+
if (
26+
selector.startsWith("#pd-embed-sidebar") ||
27+
selector.startsWith("#paper-debugger-root") ||
28+
selector.startsWith(".pd-rnd")
29+
) {
30+
return prefix + selector;
31+
}
32+
return prefixedSelector;
33+
},
34+
},
35+
}),
436
autoprefixer: {},
537
},
638
};

webapp/_webapp/src/components/markdown.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { memo } from "react";
22
import { Streamdown } from "streamdown";
3-
import { code } from "@streamdown/code";
3+
import { code } from "../libs/code-plugin";
44
import { mermaid } from "@streamdown/mermaid";
55
import { math } from "@streamdown/math";
66
import { cjk } from "@streamdown/cjk";

webapp/_webapp/src/components/message-entry-container/tools/general.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { cn } from "@heroui/react";
22
import { useEffect, useState, useRef } from "react";
33
import { Streamdown } from "streamdown";
4-
import { code } from "@streamdown/code";
4+
import { code } from "../../../libs/code-plugin";
55
import { mermaid } from "@streamdown/mermaid";
66
import { math } from "@streamdown/math";
77
import { cjk } from "@streamdown/cjk";

webapp/_webapp/src/components/pd-app-container.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ export function PdAppContainer({
77
...props
88
}: React.HTMLAttributes<HTMLDivElement> & { children: ReactNode }) {
99
return (
10-
<div className={cn("pd-app-container", className)} {...props}>
10+
<div className={cn("pd-app-container", className)} id="pd-container" {...props}>
1111
{children}
1212
</div>
1313
);

webapp/_webapp/src/components/top-menu-button.tsx

Lines changed: 29 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -50,33 +50,42 @@ export const TopMenuButton = () => {
5050
id="paper-debugger-button"
5151
onContextMenu={(event) => handleContextMenu(event)}
5252
>
53+
{/* Native Overleaf button stays OUTSIDE .pd-scope so Overleaf's own color
54+
rules win (our preflight would otherwise reset its color to black).
55+
Layout utilities are inlined; inner content is scoped for our utilities. */}
5356
<button
54-
className="btn btn-full-height flex gap-1 items-center justify-center ide-redesign-toolbar-dropdown-toggle-subdued ide-redesign-toolbar-button-subdued menu-bar-toggle"
57+
className="btn btn-full-height ide-redesign-toolbar-dropdown-toggle-subdued ide-redesign-toolbar-button-subdued menu-bar-toggle"
58+
style={{ display: "flex", gap: "0.25rem", alignItems: "center", justifyContent: "center" }}
5559
onClick={() => setIsOpen(!isOpen)}
5660
>
57-
<Logo className="bg-transparent p-0 m-0 flex items-center justify-center w-6 h-6 align-middle" />
58-
<p className={`text-exo-2 toolbar-label ${settings?.fullWidthPaperDebuggerButton ? "" : "hidden"}`}>
59-
<span className="font-light">Paper</span>
60-
<span className="font-bold">Debugger</span>
61-
<span className="text-xs text-white bg-gray-700 rounded-md px-2 py-1 ms-2">
62-
{getBrowser() === Browser.Chrome ? "⌘ + L" : "⌃ + L"}
63-
</span>
64-
</p>
61+
{/* Use Overleaf's button color var so the logo (fill=currentColor) and label track the toolbar theme. */}
62+
<span className="pd-scope" style={{ display: "contents", color: "var(--bs-btn-color)" }}>
63+
<Logo className="bg-transparent p-0 m-0 flex items-center justify-center w-6 h-6 align-middle" />
64+
<p className={`text-exo-2 toolbar-label ${settings?.fullWidthPaperDebuggerButton ? "" : "hidden"}`}>
65+
<span className="font-light">Paper</span>
66+
<span className="font-bold">Debugger</span>
67+
<span className="text-xs text-white bg-gray-700 rounded-md px-2 py-1 ms-2">
68+
{getBrowser() === Browser.Chrome ? "⌘ + L" : "⌃ + L"}
69+
</span>
70+
</p>
71+
</span>
6572
</button>
6673
{/* Position reset menu */}
67-
<div
68-
className={`pd-context-menu noselect ${contextMenuVisible ? "show" : ""}`}
69-
style={{
70-
zIndex: 998,
71-
}}
72-
>
73-
<div className="pd-context-menu-item-group">
74-
<p className="text-xs text-gray-400 px-2">If you can't find the PaperDebugger window, try to:</p>
75-
<div className="pd-context-menu-item noselect" onClick={handleResetPosition}>
76-
<p>Reset Position</p>
74+
<span className="pd-scope" style={{ display: "contents" }}>
75+
<div
76+
className={`pd-context-menu noselect ${contextMenuVisible ? "show" : ""}`}
77+
style={{
78+
zIndex: 998,
79+
}}
80+
>
81+
<div className="pd-context-menu-item-group">
82+
<p className="text-xs text-gray-400 px-2">If you can't find the PaperDebugger window, try to:</p>
83+
<div className="pd-context-menu-item noselect" onClick={handleResetPosition}>
84+
<p>Reset Position</p>
85+
</div>
7786
</div>
7887
</div>
79-
</div>
88+
</span>
8089
</div>
8190
);
8291
};

webapp/_webapp/src/devtool/index.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
></div>
2828

2929
<!-- Paper Debugger Root -->
30-
<div id="root-paper-debugger" class="hidden"></div>
30+
<div id="paper-debugger-root" class="hidden"></div>
3131
</div>
3232
</div>
3333
</div>

webapp/_webapp/src/devtool/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { AdapterProvider, getOverleafAdapter } from "../adapters";
77
const devTool = createRoot(document.getElementById("root-devtools")!);
88
devTool.render(<App />);
99

10-
const paperDebugger = createRoot(document.getElementById("root-paper-debugger")!);
10+
const paperDebugger = createRoot(document.getElementById("paper-debugger-root")!);
1111
const adapter = getOverleafAdapter();
1212
paperDebugger.render(
1313
<Providers>

webapp/_webapp/src/hooks/useThemeSync.ts

Lines changed: 12 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
import { useEffect } from "react";
22
import { useSettingStore } from "../stores/setting-store";
33

4-
const THEME_ROOT_ID = "paper-debugger-root";
5-
6-
function getThemeRoot(): HTMLElement {
7-
return document.getElementById(THEME_ROOT_ID) ?? document.documentElement;
8-
}
4+
// Every container that carries .pd-scope; the .dark class must sit on these so
5+
// scoped heroui rules (.pd-scope.dark ...) resolve. Falls back to <html> on
6+
// standalone pages (settings/popup) where no scope root exists.
7+
const SCOPE_ROOT_IDS = ["paper-debugger-root", "pd-portal", "pd-embed-sidebar"];
98

109
function applyThemeToElement(el: HTMLElement, isDark: boolean): void {
1110
if (isDark) {
@@ -15,37 +14,31 @@ function applyThemeToElement(el: HTMLElement, isDark: boolean): void {
1514
}
1615
}
1716

18-
/**
19-
* Apply theme to all elements that may contain our UI.
20-
* In Overleaf embed mode, the sidebar is rendered via portal into #pd-embed-sidebar
21-
* (inside .ide-redesign-body), which is outside #paper-debugger-root. So we must
22-
* also set the theme on documentElement so that portal content gets dark mode.
23-
*/
24-
function applyTheme(root: HTMLElement, isDark: boolean): void {
25-
applyThemeToElement(root, isDark);
26-
if (root.id === THEME_ROOT_ID && root !== document.documentElement) {
17+
function applyTheme(isDark: boolean): void {
18+
const roots = SCOPE_ROOT_IDS.map((id) => document.getElementById(id)).filter((el): el is HTMLElement => !!el);
19+
if (roots.length === 0) {
2720
applyThemeToElement(document.documentElement, isDark);
21+
return;
2822
}
23+
for (const root of roots) applyThemeToElement(root, isDark);
2924
}
3025

3126
export function useThemeSync(): void {
3227
const themeMode = useSettingStore((s) => s.themeMode);
3328

3429
useEffect(() => {
35-
const root = getThemeRoot();
36-
3730
if (themeMode === "light") {
38-
applyTheme(root, false);
31+
applyTheme(false);
3932
return;
4033
}
4134
if (themeMode === "dark") {
42-
applyTheme(root, true);
35+
applyTheme(true);
4336
return;
4437
}
4538

4639
// themeMode === "auto": follow system
4740
const media = window.matchMedia("(prefers-color-scheme: dark)");
48-
const update = () => applyTheme(root, media.matches);
41+
const update = () => applyTheme(media.matches);
4942
update();
5043
media.addEventListener("change", update);
5144
return () => media.removeEventListener("change", update);

0 commit comments

Comments
 (0)