From 6af1d79cd69d5e64a5261c11b9bfb468363daf4b Mon Sep 17 00:00:00 2001
From: Kristina <7614497+khjrtbrg@users.noreply.github.com>
Date: Mon, 27 Jul 2026 10:05:58 +0200
Subject: [PATCH] [web] reset all css variables impacting the comment field
(#35681)
GitOrigin-RevId: b66f612e186434208a658d64ccc6346c37f23182
---
.../source-editor/extensions/mentions.ts | 89 +-------------
.../js/shared/components/mentions-input.tsx | 70 +++++++++--
.../web/frontend/stylesheets/pages/all.scss | 1 +
.../pages/editor/mentions-autocomplete.scss | 109 ++++++++++++++++++
.../pages/editor/review-panel.scss | 62 +++-------
.../review-panel/review-panel.spec.tsx | 37 ++++--
6 files changed, 212 insertions(+), 156 deletions(-)
create mode 100644 services/web/frontend/stylesheets/pages/editor/mentions-autocomplete.scss
diff --git a/services/web/frontend/js/features/source-editor/extensions/mentions.ts b/services/web/frontend/js/features/source-editor/extensions/mentions.ts
index 004d761324..7eb1a35566 100644
--- a/services/web/frontend/js/features/source-editor/extensions/mentions.ts
+++ b/services/web/frontend/js/features/source-editor/extensions/mentions.ts
@@ -68,92 +68,13 @@ export function mentionCompletions(context: CompletionContext) {
// Marker class added to the mentions autocomplete tooltip (via the
// `tooltipClass` option in mentions-input.tsx). The tooltip is parented to
-// `document.body`, so the source editor's global autocomplete base theme
-// (auto-complete.ts) also lands on it. Scoping every rule under this class
-// both isolates the mentions styling from the LaTeX autocomplete and raises
-// specificity enough to win over those global `li[role="option"]` /
-// `.cm-completionDetail` rules. The `li[role="option"]` selector is used for
-// the same reason.
+// `document.body`, so it lives in the light DOM even when the editor is mounted
+// in a shadow root. Its styling therefore lives in document-level SCSS
+// (stylesheets/pages/editor/mentions-autocomplete.scss), scoped under this
+// class so it can reach the body-parented tooltip regardless of where the
+// editor is mounted.
export const MENTIONS_TOOLTIP_CLASS = 'ol-cm-mentions-autocomplete'
-export const mentionAutocompleteTheme = EditorView.baseTheme({
- '.cm-tooltip.cm-tooltip-autocomplete.ol-cm-mentions-autocomplete': {
- marginLeft: '0',
- border: 'none',
- background: 'transparent',
- borderRadius: 'var(--border-radius-large)',
- },
- '.cm-tooltip.cm-tooltip-autocomplete.ol-cm-mentions-autocomplete > ul': {
- maxWidth: 'min(360px, 90vw)',
- whiteSpace: 'normal',
- padding: 'var(--spacing-02)',
- border: '1px solid var(--border-divider)',
- borderRadius: 'var(--border-radius-large)',
- background: 'var(--bg-light-primary)',
- // Matches the `shadow-md` elevation mixin (tooltips/dropdowns).
- boxShadow: '0 4px 12px 0 rgb(30 37 48 / 12%), 0 2px 4px rgb(30 37 48 / 8%)',
- fontFamily: 'var(--font-sans)',
- },
- '.cm-tooltip.cm-tooltip-autocomplete.ol-cm-mentions-autocomplete > ul > li[role="option"]':
- {
- display: 'grid',
- gridTemplateColumns: 'auto 1fr',
- gridTemplateRows: 'auto auto',
- alignItems: 'center',
- columnGap: 'var(--spacing-04)',
- padding: 'var(--spacing-03) var(--spacing-04)',
- overflowWrap: 'anywhere',
- borderRadius: 'var(--border-radius-medium)',
- lineHeight: '1.3',
- },
- '.cm-tooltip.cm-tooltip-autocomplete.ol-cm-mentions-autocomplete > ul > li[role="option"][aria-selected]':
- {
- background: 'var(--bg-light-secondary)',
- },
-
- '.cm-tooltip.cm-tooltip-autocomplete.ol-cm-mentions-autocomplete .ol-cm-mention-avatar':
- {
- gridColumn: '1',
- gridRow: '1 / span 2',
- alignSelf: 'center',
- display: 'flex',
- alignItems: 'center',
- justifyContent: 'center',
- width: '32px',
- height: '32px',
- borderRadius: 'var(--border-radius-full)',
- color: 'var(--white)',
- fontSize: 'var(--font-size-02)',
- fontWeight: '600',
- textTransform: 'uppercase',
- boxSizing: 'border-box',
- },
- '.cm-tooltip.cm-tooltip-autocomplete.ol-cm-mentions-autocomplete .cm-completionLabel':
- {
- gridColumn: '2',
- gridRow: '1',
- fontSize: 'var(--font-size-03)',
- color: 'var(--content-primary)',
- },
- // The portion of the label matching what the user has typed.
- '.cm-tooltip.cm-tooltip-autocomplete.ol-cm-mentions-autocomplete .cm-completionMatchedText':
- {
- fontWeight: '700',
- textDecoration: 'none',
- },
- // details
- '.cm-tooltip.cm-tooltip-autocomplete.ol-cm-mentions-autocomplete .cm-completionDetail':
- {
- gridColumn: '2',
- gridRow: '2',
- margin: '0',
- fontFamily: 'var(--font-sans)',
- fontSize: 'var(--font-size-02)',
- fontStyle: 'normal',
- color: 'var(--content-secondary)',
- },
-})
-
// Renders a project member's avatar circle for the autocomplete option. The
// background uses the same per-user hue as the rest of the editor's
// collaborator colours (via getHueForUserId), so a user looks consistent
diff --git a/services/web/frontend/js/shared/components/mentions-input.tsx b/services/web/frontend/js/shared/components/mentions-input.tsx
index cc5e80ab95..be50b14b7b 100644
--- a/services/web/frontend/js/shared/components/mentions-input.tsx
+++ b/services/web/frontend/js/shared/components/mentions-input.tsx
@@ -12,7 +12,6 @@ import { autocompletion, completionStatus } from '@codemirror/autocomplete'
import classNames from 'classnames'
import mentions, {
mentionCompletions,
- mentionAutocompleteTheme,
renderMentionAvatar,
MENTIONS_TOOLTIP_CLASS,
} from '@/features/source-editor/extensions/mentions'
@@ -43,16 +42,20 @@ type MentionsInputProps = {
className?: string
}
-// Shared editor styling for the mentions input.
+// Styling for the mentions input's own CodeMirror instance. Mounted in a shadow
+// root, so the document editor's selector-based CSS cannot reach it; only
+// inherited custom properties cross the boundary. These rules override CM's
+// built-in base theme, injected into the shadow root alongside the view.
const mentionsInputTheme = EditorView.theme({
- // reset variables to override visual mode's .cm-content rules
- '&.cm-editor': {
- '--visual-font-family': 'var(--bs-body-font-family)',
- '--visual-font-size': 'var(--font-size)',
- },
- // double the specificity to override the base font family used by theme.ts regardless of extension load order
- '.cm-content.cm-content': {
+ // Match the surrounding UI's body font rather than CM6's inherited defaults.
+ '&.cm-editor .cm-content': {
fontFamily: 'var(--bs-body-font-family)',
+ fontSize: 'inherit',
+ lineHeight: 'inherit',
+ },
+ // suppress CM6's default focus outline; the consumer's border/parent conveys focus
+ '&.cm-editor.cm-focused': {
+ outline: 'none',
},
// when range in editor is focused and selected
'&.cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground': {
@@ -67,6 +70,43 @@ const mentionsInputTheme = EditorView.theme({
},
})
+// Appearance for the review-panel inputs. These are mounted in a shadow root,
+// so the light-DOM SCSS in review-panel.scss that used to style
+// the inner CodeMirror via descendant selectors can no longer reach it. That
+// styling lives here instead. The host
still gets its box styling
+// (background/border/padding) from review-panel.scss, and CSS custom properties
+// (e.g. --review-panel-color, --rp-input-min-height) cross the shadow boundary,
+// so those rules resolve against the review-panel context as before.
+const reviewPanelInputTheme = EditorView.theme({
+ '&.cm-editor': {
+ background: 'transparent',
+ color: 'inherit',
+ maxHeight: '200px',
+ },
+ '.cm-scroller': {
+ lineHeight: 'var(--line-height-02)',
+ // Hide scrollbars so they don't consume layout width on systems that use
+ // non-overlay scrollbars (e.g. macOS "always show scrollbars"), which would
+ // otherwise trigger extra remeasure cycles. Scrolling still works via
+ // trackpad/keyboard.
+ scrollbarWidth: 'none',
+ '&::-webkit-scrollbar': {
+ display: 'none',
+ },
+ },
+ '.cm-content': {
+ // --rp-input-min-height is set per-variant on the host in review-panel.scss.
+ minHeight: 'var(--rp-input-min-height, 44px)',
+ padding: 'var(--spacing-01) var(--spacing-03)',
+ },
+ '.cm-cursor, .cm-cursor-primary': {
+ borderLeftColor: 'var(--review-panel-color)',
+ },
+ '.cm-placeholder': {
+ color: 'var(--content-placeholder-themed)',
+ },
+})
+
// A small CodeMirror 6 input that supports `@mention` autocompletion. Shared by
// the review panel's add-comment, reply, and edit fields so all of them get the
// same mention behaviour.
@@ -137,14 +177,23 @@ export const MentionsInput = forwardRef<
return
}
+ // Review-panel inputs are portaled inside the document editor's DOM, so
+ // mount inside a shadow root to keep the editor's selector-based CSS out.
+ // attachShadow throws if a root already exists (e.g. StrictMode remount),
+ // so reuse the existing one.
+ const parent =
+ containerRef.current.shadowRoot ??
+ containerRef.current.attachShadow({ mode: 'open' })
+
const view = new EditorView({
doc: initialValue,
- parent: containerRef.current,
+ parent,
extensions: [
projectMembersInfo,
EditorView.lineWrapping,
drawSelection(),
mentionsInputTheme,
+ reviewPanelInputTheme,
placeholderExt(placeholder),
...(mentionsEnabled
? [
@@ -157,7 +206,6 @@ export const MentionsInput = forwardRef<
icons: false,
addToOptions: [{ render: renderMentionAvatar, position: 20 }],
}),
- mentionAutocompleteTheme,
]
: []),
editableConf.current.of(EditorView.editable.of(!disabled)),
diff --git a/services/web/frontend/stylesheets/pages/all.scss b/services/web/frontend/stylesheets/pages/all.scss
index ef0db13379..4b0224b767 100644
--- a/services/web/frontend/stylesheets/pages/all.scss
+++ b/services/web/frontend/stylesheets/pages/all.scss
@@ -21,6 +21,7 @@
@import 'editor/file-view';
@import 'editor/figure-modal';
@import 'editor/chat';
+@import 'editor/mentions-autocomplete';
@import 'editor/toast';
@import 'editor/history';
@import 'editor/write-and-cite';
diff --git a/services/web/frontend/stylesheets/pages/editor/mentions-autocomplete.scss b/services/web/frontend/stylesheets/pages/editor/mentions-autocomplete.scss
new file mode 100644
index 0000000000..d191cfcef0
--- /dev/null
+++ b/services/web/frontend/stylesheets/pages/editor/mentions-autocomplete.scss
@@ -0,0 +1,109 @@
+/*
+ * Styling for the @mention autocomplete tooltip used by the review-panel
+ * comment inputs (see mentions-input.tsx). The tooltip is
+ * parented to `document.body` (via `tooltips({ parent: document.body })`), so it
+ * lives in the light DOM even though the editor itself is mounted in a shadow
+ * root. Its styling must therefore live here, at document level, rather than in
+ * an in-editor CodeMirror theme.
+ *
+ * Because the shadow-hosted editor's CodeMirror base theme cannot reach a
+ * body-parented tooltip, this block is self-contained: it re-declares
+ * CodeMirror's autocomplete `baseTheme` rules for `.cm-tooltip` /
+ * `.cm-tooltip-autocomplete > ul` (z-index, list layout, scrolling), which CM
+ * injects into the editor's root (the shadow root here) and so never reach a
+ * `document.body`-parented tooltip.
+ *
+ * Every rule is scoped under the triple class to both isolate the mentions
+ * styling from the LaTeX autocomplete and win specificity over the source
+ * editor's global `li[role="option"]` / `.cm-completionDetail` rules.
+ */
+.cm-tooltip.cm-tooltip-autocomplete.ol-cm-mentions-autocomplete {
+ // Load-bearing CM base rules, re-declared so the tooltip is self-contained.
+ z-index: 500;
+ box-sizing: border-box;
+ margin-left: 0;
+ border: none;
+ background: transparent;
+ border-radius: var(--border-radius-large);
+
+ > ul {
+ overflow: hidden auto;
+ max-height: 10em;
+ list-style: none;
+ margin: 0;
+ height: 100%;
+ max-width: min(360px, 90vw);
+ white-space: normal;
+ padding: var(--spacing-02);
+ border: 1px solid var(--border-divider);
+ border-radius: var(--border-radius-large);
+ background: var(--bg-light-primary);
+
+ // Matches the `shadow-md` elevation mixin (tooltips/dropdowns).
+ box-shadow:
+ 0 4px 12px 0 rgb(30 37 48 / 12%),
+ 0 2px 4px rgb(30 37 48 / 8%);
+ font-family: var(--font-sans);
+
+ > li[role='option'] {
+ cursor: pointer;
+ overflow-x: hidden;
+ text-overflow: ellipsis;
+ display: grid;
+ grid-template-columns: auto 1fr;
+ grid-template-rows: auto auto;
+ align-items: center;
+ column-gap: var(--spacing-04);
+ padding: var(--spacing-03) var(--spacing-04);
+ overflow-wrap: anywhere;
+ border-radius: var(--border-radius-medium);
+ line-height: 1.3;
+
+ &[aria-selected] {
+ background: var(--bg-light-secondary);
+ }
+ }
+ }
+
+ .ol-cm-mention-avatar {
+ grid-column: 1;
+ grid-row: 1 / span 2;
+ align-self: center;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 32px;
+ height: 32px;
+ border-radius: var(--border-radius-full);
+ color: var(--white);
+ font-size: var(--font-size-02);
+ font-weight: 600;
+ text-transform: uppercase;
+ box-sizing: border-box;
+ }
+
+ /* stylelint-disable selector-class-pattern -- CodeMirror completion classes */
+ .cm-completionLabel {
+ grid-column: 2;
+ grid-row: 1;
+ font-size: var(--font-size-03);
+ color: var(--content-primary);
+ }
+
+ // The portion of the label matching what the user has typed.
+ .cm-completionMatchedText {
+ font-weight: 700;
+ text-decoration: none;
+ }
+
+ .cm-completionDetail {
+ grid-column: 2;
+ grid-row: 2;
+ margin: 0;
+ font-family: var(--font-sans);
+ font-size: var(--font-size-02);
+ font-style: normal;
+ color: var(--content-secondary);
+ }
+ /* stylelint-enable selector-class-pattern */
+}
diff --git a/services/web/frontend/stylesheets/pages/editor/review-panel.scss b/services/web/frontend/stylesheets/pages/editor/review-panel.scss
index 3caa599e45..b85d3963c2 100644
--- a/services/web/frontend/stylesheets/pages/editor/review-panel.scss
+++ b/services/web/frontend/stylesheets/pages/editor/review-panel.scss
@@ -488,7 +488,15 @@ del.review-panel-content-highlight {
}
}
+// Host box for the review-panel comment inputs. The inner CodeMirror is mounted
+// in a shadow root, so its styling lives in an
+// in-shadow CM theme (reviewPanelInputTheme in mentions-input.tsx), not here.
+// Only host-box rules and CSS custom properties (which cross the shadow
+// boundary) belong in this block. --rp-input-min-height feeds the inner
+// .cm-content min-height inside the shadow.
.review-panel-add-comment-editor {
+ --rp-input-min-height: 44px;
+
background: var(--review-panel-entry-bg-color);
color: var(--review-panel-color);
border: 1px solid var(--border-primary-themed);
@@ -497,57 +505,13 @@ del.review-panel-content-highlight {
cursor: text;
&.review-panel-comment-input {
+ // Default to two lines tall so long @mentions wrap into view
+ --rp-input-min-height: calc(
+ (2 * var(--line-height-02)) + (2 * var(--spacing-01))
+ );
+
font-size: var(--rp-base-font-size);
color: var(--review-panel-color-secondary);
-
- .cm-content {
- // Default to two lines tall so long @mentions wrap into view
- min-height: calc((2 * var(--line-height-02)) + (2 * var(--spacing-01)));
- }
- }
-
- .cm-editor {
- background: transparent;
- color: inherit;
- max-height: 200px;
-
- &.cm-focused {
- outline: none;
- }
- }
-
- .cm-scroller {
- font-family: inherit;
- line-height: var(--line-height-02);
-
- // Hide scrollbars so they don't consume layout width on systems that use
- // non-overlay scrollbars (e.g. macOS "always show scrollbars"). A visible
- // scrollbar causes a width change mid-init that fires CM's ResizeObserver,
- // which triggers extra remeasure/reposition cycles — visible as a "dance".
- // Scrolling still works via trackpad/keyboard.
- scrollbar-width: none;
-
- &::-webkit-scrollbar {
- display: none;
- }
- }
-
- .cm-content {
- min-height: 44px;
- }
-
- // extra specificity to override visual mode's .cm-content rules
- &.review-panel-add-comment-editor .cm-content.cm-content {
- padding: var(--spacing-01) var(--spacing-03);
- }
-
- .cm-cursor,
- .cm-cursor-primary {
- border-left-color: var(--review-panel-color);
- }
-
- .cm-placeholder {
- color: var(--content-placeholder-themed);
}
}
diff --git a/services/web/test/frontend/features/review-panel/review-panel.spec.tsx b/services/web/test/frontend/features/review-panel/review-panel.spec.tsx
index 7a47169f18..4714b3bca0 100644
--- a/services/web/test/frontend/features/review-panel/review-panel.spec.tsx
+++ b/services/web/test/frontend/features/review-panel/review-panel.spec.tsx
@@ -339,10 +339,14 @@ describe('', function () {
cy.findByRole('menu').within(() => {
cy.findByText('Edit').click({ scrollBehavior: false })
})
- cy.findByRole('textbox').type(
- '{selectAll}edited comment text{enter}',
- { scrollBehavior: false }
- )
+ // The edit input is mounted in a shadow root, so pierce it to
+ // reach the CodeMirror content.
+ cy.get('.review-panel-comment-edit')
+ .shadow()
+ .find('.cm-content')
+ .type('{selectAll}edited comment text{enter}', {
+ scrollBehavior: false,
+ })
cy.wait('@editComment')
// TODO: Figure out a way to plumb the websocket response back through
// to the test so we can verify the comment is resolved
@@ -422,9 +426,14 @@ describe('', function () {
it('adds new comment (replies) to a thread', function () {
cy.get('@review-panel').within(() => {
- cy.findByRole('textbox').type('a new reply{enter}', {
- scrollBehavior: false,
- })
+ // The reply input is mounted in a shadow root, so pierce it to reach
+ // the CodeMirror content.
+ cy.get('.review-panel-add-comment-editor.review-panel-comment-input')
+ .shadow()
+ .find('.cm-content')
+ .type('a new reply{enter}', {
+ scrollBehavior: false,
+ })
})
cy.wait('@addReply')
})
@@ -518,12 +527,16 @@ describe('', function () {
cy.get('@add-comment-button').click({ scrollBehavior: false })
cy.get('@review-panel').within(() => {
// The add-comment editor is the CM6 input that is not a reply/edit
- // input (those also carry the review-panel-comment-input class).
+ // input (those also carry the review-panel-comment-input class). It is
+ // mounted in a shadow root, so pierce it to reach the content.
cy.get(
- '.review-panel-add-comment-editor:not(.review-panel-comment-input) .cm-content'
- ).type('a new comment{enter}', {
- scrollBehavior: false,
- })
+ '.review-panel-add-comment-editor:not(.review-panel-comment-input)'
+ )
+ .shadow()
+ .find('.cm-content')
+ .type('a new comment{enter}', {
+ scrollBehavior: false,
+ })
})
cy.wait('@addNewComment')
// TODO : Figure out a way to plumb the websocket response back through