Merge "ApprovalCopier: Log the result of the copy evaluation in traces"
diff --git a/polygerrit-ui/app/api/plugin.ts b/polygerrit-ui/app/api/plugin.ts
index 733f112..3ed48e6 100644
--- a/polygerrit-ui/app/api/plugin.ts
+++ b/polygerrit-ui/app/api/plugin.ts
@@ -14,6 +14,7 @@
 import {ChangeActionsPluginApi} from './change-actions';
 import {RestPluginApi} from './rest';
 import {HookApi, RegisterOptions} from './hook';
+import {StylePluginApi} from './styles';
 
 export enum TargetElement {
   CHANGE_ACTIONS = 'changeactions',
@@ -77,10 +78,11 @@
     moduleName?: string,
     options?: RegisterOptions
   ): HookApi<T>;
-  // DEPRECATED: Just add <style> elements to `document.head`.
+  // DEPRECATED: Use styleApi() instead.
   registerStyleModule(endpoint: string, moduleName: string): void;
   reporting(): ReportingPluginApi;
   restApi(): RestPluginApi;
   // eslint-disable-next-line @typescript-eslint/no-explicit-any
   screen(screenName: string, moduleName?: string): any;
+  styleApi(): StylePluginApi;
 }
diff --git a/polygerrit-ui/app/api/styles.ts b/polygerrit-ui/app/api/styles.ts
index f5b22a1..6ca8496 100644
--- a/polygerrit-ui/app/api/styles.ts
+++ b/polygerrit-ui/app/api/styles.ts
@@ -22,6 +22,7 @@
   toString(): string;
 }
 
+/** Accessible via `window.Gerrit.styles`. */
 export declare interface Styles {
   font: Style;
   form: Style;
@@ -32,3 +33,22 @@
   table: Style;
   modal: Style;
 }
+
+/** Accessible via `window.Gerrit.install(plugin => {plugin.styleApi()})`. */
+export declare interface StylePluginApi {
+  /**
+   * Convenience method for adding a CSS rule to a <style> element in <head>.
+   *
+   * Note that you can only insert one rule per call. See `insertRule()`
+   * documentation of `CSSStyleSheet`:
+   * https://developer.mozilla.org/en-US/docs/Web/API/CSSStyleSheet/insertRule
+   *
+   * @param rule the css rule, e.g.:
+   *        ```
+   *          html.darkTheme {
+   *            --header-background-color: blue;
+   *          }
+   *        ```
+   */
+  insertCSSRule(rule: string): void;
+}
diff --git a/polygerrit-ui/app/elements/core/gr-notifications-prompt/gr-notifications-prompt.ts b/polygerrit-ui/app/elements/core/gr-notifications-prompt/gr-notifications-prompt.ts
index e01bb34..ab95711 100644
--- a/polygerrit-ui/app/elements/core/gr-notifications-prompt/gr-notifications-prompt.ts
+++ b/polygerrit-ui/app/elements/core/gr-notifications-prompt/gr-notifications-prompt.ts
@@ -51,7 +51,7 @@
         #notificationsPrompt {
           position: absolute;
           right: 30px;
-          top: 100px;
+          top: 50px;
           z-index: 150; /* Less than gr-hovercard's, higher than rest */
           display: flex;
           background-color: var(--background-color-primary);
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-style-api.ts b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-style-api.ts
new file mode 100644
index 0000000..13eefc8
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-style-api.ts
@@ -0,0 +1,43 @@
+/**
+ * @license
+ * Copyright 2022 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import {PluginApi} from '../../../api/plugin';
+import {StylePluginApi} from '../../../api/styles';
+import {ReportingService} from '../../../services/gr-reporting/gr-reporting';
+
+function getOrCreatePluginStyleEl(): HTMLStyleElement {
+  const el =
+    document.head.querySelector<HTMLStyleElement>('style#plugin-style');
+  if (el) return el;
+
+  const styleEl = document.createElement('style');
+  styleEl.setAttribute('id', 'plugin-style');
+  // Append at the end so that they override the default light and dark theme
+  // styles.
+  document.head.appendChild(styleEl);
+  return styleEl;
+}
+
+export class GrPluginStyleApi implements StylePluginApi {
+  constructor(
+    private readonly reporting: ReportingService,
+    private readonly plugin: PluginApi
+  ) {
+    this.reporting.trackApi(this.plugin, 'style', 'constructor');
+  }
+
+  insertCSSRule(rule: string): void {
+    this.reporting.trackApi(this.plugin, 'style', 'insertCSSRule');
+
+    const styleEl = getOrCreatePluginStyleEl();
+    try {
+      styleEl.sheet?.insertRule(rule);
+    } catch (error) {
+      console.error(
+        `Failed to insert CSS rule for plugin ${this.plugin.getPluginName()}: ${error}`
+      );
+    }
+  }
+}
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-style-api_test.ts b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-style-api_test.ts
new file mode 100644
index 0000000..469d667
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-style-api_test.ts
@@ -0,0 +1,51 @@
+/**
+ * @license
+ * Copyright 2022 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import '../../../test/common-test-setup';
+import './gr-js-api-interface';
+import {query, queryAndAssert} from '../../../utils/common-util';
+import {assert} from '@open-wc/testing';
+import {StylePluginApi} from '../../../api/styles';
+
+suite('gr-plugin-style-api tests', () => {
+  let styleApi: StylePluginApi;
+
+  setup(() => {
+    window.Gerrit.install(
+      p => (styleApi = p.styleApi()),
+      '0.1',
+      'http://test.com/plugins/testplugin/static/test.js'
+    );
+  });
+
+  teardown(() => {
+    const styleEl = query<HTMLStyleElement>(
+      document.head,
+      'style#plugin-style'
+    );
+    styleEl?.remove();
+  });
+
+  test('insertCSSRule adds a rule', async () => {
+    styleApi.insertCSSRule('html{color:green;}');
+    const styleEl = queryAndAssert<HTMLStyleElement>(
+      document.head,
+      'style#plugin-style'
+    );
+    const styleSheet = styleEl.sheet;
+    assert.equal(styleSheet?.cssRules.length, 1);
+  });
+
+  test('insertCSSRule re-uses the <style> element', async () => {
+    styleApi.insertCSSRule('html{color:green;}');
+    styleApi.insertCSSRule('html{margin:0px;}');
+    const styleEl = queryAndAssert<HTMLStyleElement>(
+      document.head,
+      'style#plugin-style'
+    );
+    const styleSheet = styleEl.sheet;
+    assert.equal(styleSheet?.cssRules.length, 2);
+  });
+});
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-public-js-api.ts b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-public-js-api.ts
index 2ed3794..7170051 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-public-js-api.ts
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-public-js-api.ts
@@ -35,6 +35,8 @@
 import {ReportingService} from '../../../services/gr-reporting/gr-reporting';
 import {RestApiService} from '../../../services/gr-rest-api/gr-rest-api';
 import {PluginsModel} from '../../../models/plugins/plugins-model';
+import {GrPluginStyleApi} from './gr-plugin-style-api';
+import {StylePluginApi} from '../../../api/styles';
 
 /**
  * Plugin-provided custom components can affect content in extension
@@ -244,6 +246,10 @@
     return new GrReportingJsApi(this.report, this);
   }
 
+  styleApi(): StylePluginApi {
+    return new GrPluginStyleApi(this.report, this);
+  }
+
   admin(): AdminPluginApi {
     return new GrAdminApi(this.report, this);
   }
diff --git a/polygerrit-ui/app/mixins/hovercard-mixin/hovercard-mixin.ts b/polygerrit-ui/app/mixins/hovercard-mixin/hovercard-mixin.ts
index c8f4fa6..b383fd7 100644
--- a/polygerrit-ui/app/mixins/hovercard-mixin/hovercard-mixin.ts
+++ b/polygerrit-ui/app/mixins/hovercard-mixin/hovercard-mixin.ts
@@ -219,6 +219,7 @@
         );
       }
       this.addEventListener('request-dependency', this.resolveDep);
+      this.addEventListener('reload', this.reload);
     }
 
     private removeTargetEventListeners() {
@@ -231,6 +232,7 @@
       }
       this.targetCleanups = [];
       this.removeEventListener('request-dependency', this.resolveDep);
+      this.removeEventListener('reload', this.reload);
     }
 
     /**
@@ -246,6 +248,10 @@
       }
     }
 
+    readonly reload = () => {
+      this.dispatchEventThroughTarget('reload');
+    };
+
     readonly mouseDebounceHide = (e: MouseEvent) => {
       this.debounceHide({mouseEvent: e});
     };
diff --git a/polygerrit-ui/app/styles/themes/dark-theme.ts b/polygerrit-ui/app/styles/themes/dark-theme.ts
index 2836247..4477fd3 100644
--- a/polygerrit-ui/app/styles/themes/dark-theme.ts
+++ b/polygerrit-ui/app/styles/themes/dark-theme.ts
@@ -294,7 +294,13 @@
   const styleEl = document.createElement('style');
   styleEl.setAttribute('id', 'dark-theme');
   safeStyleEl.setTextContent(styleEl, darkThemeCss);
-  document.head.appendChild(styleEl);
+
+  // We would like to insert the dark theme styles after the light theme such
+  // that the dark theme values override the defaults in the light theme. But
+  // OTOH we want to insert before any plugin provided styles, because we do NOT
+  // want to override those.
+  const pluginStyleEl = document.head.querySelector('style#plugin-style');
+  document.head.insertBefore(styleEl, pluginStyleEl);
 }
 
 export function removeTheme() {