Repository: climatepolicyradar/navigator-frontend
Pull request #510: APP-541: Search labelling
Author: login: jesskelsall
Created: 2025-05-12T09:56:19Z | Merged: 2025-05-12T16:17:42Z

# What's changed

- Revisions to `Popover` component:
  - Prefers passing title, description, and link props, but still supports throwing children at it.
    - Allows existing usage of Popover to be refactored (more DRY).
  - Top arrow exported from a utils file for future use with other Base UI components (Menu, Preview Card).
- Search page:
  - Added `Info` component to filters and search results.
  - Removed `NoOfResults` component (now redundant with wording changes).
  - Updated search settings from a Lucide icon to "Sort & Display" with open state UI.
- Document page:
  - Same treatment for "Sort & Display" (both inside and outside of feature flag).

## Why?

Satisfies [APP-566](https://linear.app/climate-policy-radar/issue/APP-566/concept-popover) and [APP-541](https://linear.app/climate-policy-radar/issue/APP-541/labelling).

## Screenshots?

Search page:
<img width="1577" alt="Screenshot 2025-05-12 at 09 23 28" src="https://github.com/user-attachments/assets/adb135fc-d35e-4033-ba4b-44b5dcc317da" />

Filters popover:
<img width="362" alt="Screenshot 2025-05-12 at 09 23 40" src="https://github.com/user-attachments/assets/41d93669-dbe6-4092-9f78-e882cabfb64e" />

Results popover:
<img width="566" alt="Screenshot 2025-05-12 at 09 23 48" src="https://github.com/user-attachments/assets/f53029ef-72fc-4eed-b9f6-73bee7588b04" />

Search settings:
<img width="200" alt="Screenshot 2025-05-12 at 09 23 55" src="https://github.com/user-attachments/assets/a5786b9b-f63b-4729-866f-ee45d8848cbe" />

Document page (Concepts feature flag):
<img width="1578" alt="Screenshot 2025-05-12 at 09 24 22" src="https://github.com/user-attachments/assets/930f7cfa-f219-4b69-b932-1891bd0e0e3d" />



Diff:
diff --git a/src/components/NoOfResults.tsx b/src/components/NoOfResults.tsx
deleted file mode 100644
index cf3cde283..000000000
--- a/src/components/NoOfResults.tsx
+++ /dev/null
@@ -1,32 +0,0 @@
-import { SearchLimitTooltip } from "@/components/tooltip/SearchLimitTooltip";
-
-import { MAX_RESULTS } from "@/constants/paging";
-
-type TProps = {
-  hits: number;
-  queryString: string | string[];
-};
-
-export const NoOfResults = ({ hits, queryString }: TProps) => {
-  let resultsMsg = `Showing`;
-  if (hits < MAX_RESULTS) {
-    resultsMsg += ` ${hits} results`;
-  } else {
-    resultsMsg += ` the top ${MAX_RESULTS} results`;
-  }
-  return (
-    <>
-      {resultsMsg}{" "}
-      {queryString && (
-        <>
-          for "<span className="text-textDark font-medium">{queryString}</span>"
-          {hits >= MAX_RESULTS && (
-            <div className="ml-2 inline-block">
-              <SearchLimitTooltip />
-            </div>
-          )}
-        </>
-      )}
-    </>
-  );
-};
diff --git a/src/components/atoms/popover/Popover.stories.tsx b/src/components/atoms/popover/Popover.stories.tsx
index 7b2b6dad1..f777b0dce 100644
--- a/src/components/atoms/popover/Popover.stories.tsx
+++ b/src/components/atoms/popover/Popover.stories.tsx
@@ -16,10 +16,25 @@ type Story = StoryObj<typeof Popover>;
 
 export default meta;
 
-export const Generic: Story = {
+const trigger = <Button>Trigger</Button>;
+
+export const WithChildren: Story = {
   args: {
     children: "Hello, I am a popover!",
     openOnHover: false,
-    trigger: <Button>Trigger</Button>,
+    trigger,
+  },
+};
+
+export const WithElements: Story = {
+  args: {
+    description: "This is the popover description. Lots of text can go here, if you'd like.",
+    link: {
+      href: "#",
+      text: "Link",
+    },
+    openOnHover: false,
+    title: "Title",
+    trigger,
   },
 };
diff --git a/src/components/atoms/popover/Popover.tsx b/src/components/atoms/popover/Popover.tsx
index ac7647de1..6af177739 100644
--- a/src/components/atoms/popover/Popover.tsx
+++ b/src/components/atoms/popover/Popover.tsx
@@ -1,29 +1,42 @@
+import { ExternalLink } from "@/components/ExternalLink";
+import { BaseUIArrow } from "@/utils/baseUI";
 import { joinTailwindClasses } from "@/utils/tailwind";
 import { Popover as BasePopover } from "@base-ui-components/react/popover";
+import Link from "next/link";
 import { ReactElement, ReactNode } from "react";
 
-const PopoverArrow = () => (
-  <svg width="20" height="10" viewBox="0 0 20 10" fill="none">
-    <path
-      d="M9.66437 2.60207L4.80758 6.97318C4.07308 7.63423 3.11989 8 2.13172 8H0V10H20V8H18.5349C17.5468 8 16.5936 7.63423 15.8591 6.97318L11.0023 2.60207C10.622 2.2598 10.0447 2.25979 9.66437 2.60207Z"
-      className="fill-surface-light"
-    />
-    <path
-      d="M10.3333 3.34539L5.47654 7.71648C4.55842 8.54279 3.36693 9 2.13172 9H0V8H2.13172C3.11989 8 4.07308 7.63423 4.80758 6.97318L9.66437 2.60207C10.0447 2.25979 10.622 2.2598 11.0023 2.60207L15.8591 6.97318C16.5936 7.63423 17.5468 8 18.5349 8H20V9H18.5349C17.2998 9 16.1083 8.54278 15.1901 7.71648L10.3333 3.34539Z"
-      className="fill-border-light"
-    />
-  </svg>
-);
-
-interface PopoverProps {
-  children: ReactNode;
+export type TPopoverLink = {
+  href: string;
+  text: string;
+  external?: boolean;
+};
+
+// Provide options for structuring the popover using props & Base UI components (heavily preferred) or children for full flexibility
+
+interface IPopoverGenericProps {
   onOpenChange?: (open: boolean) => void;
   openOnHover?: boolean;
   popupClasses?: string;
   trigger: ReactElement;
 }
 
-export const Popover = ({ children, onOpenChange, openOnHover = false, popupClasses = "", trigger }: PopoverProps) => {
+interface IPopoverElementProps extends IPopoverGenericProps {
+  title?: string;
+  description: string;
+  link?: TPopoverLink;
+  children?: never;
+}
+
+interface IPopoverChildrenProps extends IPopoverGenericProps {
+  children: ReactNode;
+  title?: never;
+  description?: never;
+  link?: never;
+}
+
+type TPopoverProps = IPopoverElementProps | IPopoverChildrenProps;
+
+export const Popover = ({ children, description, link, onOpenChange, openOnHover = false, popupClasses = "", title, trigger }: TPopoverProps) => {
   const allPopupClasses = joinTailwindClasses(
     "p-3 max-w-[350px] bg-surface-light border border-border-light rounded-md shadow-md text-sm leading-normal select-none focus-visible:outline-0",
     popupClasses
@@ -36,9 +49,25 @@ export const Popover = ({ children, onOpenChange, openOnHover = false, popupClas
         <BasePopover.Positioner positionMethod="fixed" sideOffset={8}>
           <BasePopover.Popup className={allPopupClasses}>
             <BasePopover.Arrow className="flex -top-2">
-              <PopoverArrow />
+              <BaseUIArrow fill="fill-surface-light" />
             </BasePopover.Arrow>
-            {children}
+            {children || (
+              <>
+                {title && <BasePopover.Title className="mb-2 font-bold">{title}</BasePopover.Title>}
+                <BasePopover.Description>
+                  <span className="block">{description}</span>
+                  {link && link.external ? (
+                    <ExternalLink url={link.href} className="block mt-2 underline">
+                      {link.text}
+                    </ExternalLink>
+                  ) : (
+                    <Link href={link.href} className="block mt-2 underline">
+                      {link.text}
+                    </Link>
+                  )}
+                </BasePopover.Description>
+              </>
+            )}
           </BasePopover.Popup>
         </BasePopover.Positioner>
       </BasePopover.Portal>
diff --git a/src/components/blocks/SearchFilters.tsx b/src/components/blocks/SearchFilters.tsx
index d66fa810d..633f65ed5 100644
--- a/src/components/blocks/SearchFilters.tsx
+++ b/src/components/blocks/SearchFilters.tsx
@@ -28,6 +28,7 @@ import { getFilterLabel } from "@/utils/getFilterLabel";
 import { TConcept, TCorpusTypeDictionary, TGeography, TSearchCriteria, TThemeConfigOption } from "@/types";
 
 import { SlideOutContext } from "@/context/SlideOutContext";
+import { Info } from "../molecules/info/Info";
 
 const isCategoryChecked = (selectedCatgeory: string | undefined, themeConfigCategory: TThemeConfigOption) => {
   if (selectedCatgeory) {
@@ -100,8 +101,13 @@ const SearchFilters = ({
     <div id="search_filters" data-cy="seach-filters" className="text-sm text-text-secondary flex flex-col gap-5">
       {themeConfigStatus === "loading" && <Loader size="20px" />}
       <div className="flex justify-between">
-        <div className="flex gap-2">
-          <p className="text-xs uppercase">Filters</p>
+        <div className="flex items-center gap-2">
+          <p className="text-[15px] text-text-primary font-normal">Filters</p>
+          <Info
+            title="About"
+            description="Narrow down your results using the filters below. You can also combine these with a search term."
+            link={{ href: "/faq", text: "Learn more" }}
+          />
         </div>
         {showClear && (
           <button className="anchor underline text-sm" onClick={handleClearSearch}>
diff --git a/src/components/documents/ConceptsDocumentViewer.tsx b/src/components/documents/ConceptsDocumentViewer.tsx
index 74e32b6e4..f478177fb 100644
--- a/src/components/documents/ConceptsDocumentViewer.tsx
+++ b/src/components/documents/ConceptsDocumentViewer.tsx
@@ -266,10 +266,13 @@ export const ConceptsDocumentViewer = ({
           )}`}
         >
           <div className="relative">
-            <div className="flex justify-between p-4">
+            <div className="flex justify-between items-end p-4">
               <h1 className="text-base font-medium">Passage matches</h1>
-              <button className="text-xl text-text-tertiary" onClick={() => setShowSearchOptions(!showSearchOptions)}>
-                <LuSettings2 />
+              <button
+                className={`px-1 py-0.5 -mt-0.5 rounded-md text-sm text-text-primary font-normal ${showSearchOptions ? "bg-surface-ui" : ""}`}
+                onClick={() => setShowSearchOptions(!showSearchOptions)}
+              >
+                Sort &amp; Display
               </button>
             </div>
             <AnimatePresence initial={false}>
diff --git a/src/components/filters/SearchSettingsItem.tsx b/src/components/filters/SearchSettingsItem.tsx
index f94d09732..c64f89bf0 100644
--- a/src/components/filters/SearchSettingsItem.tsx
+++ b/src/components/filters/SearchSettingsItem.tsx
@@ -1,3 +1,5 @@
+import { LuCheck } from "react-icons/lu";
+
 type TProps = {
   children: React.ReactNode;
   isActive?: boolean;
@@ -5,7 +7,13 @@ type TProps = {
 };
 
 export const SearchSettingsItem = ({ children, isActive, onClick, ...props }: TProps) => (
-  <a className={`text-white hover:text-white hover:opacity-100 ${isActive ? "" : "opacity-70"}`} onClick={onClick} href="#" {...props}>
+  <a
+    className={`flex gap-1 text-white items-center hover:text-white hover:opacity-100 ${isActive ? "" : "opacity-70"}`}
+    onClick={onClick}
+    href="#"
+    {...props}
+  >
+    {isActive && <LuCheck size="14" />}
     {children}
   </a>
 );
diff --git a/src/components/molecules/conceptLink/ConceptLink.tsx b/src/components/molecules/conceptLink/ConceptLink.tsx
index 9742b7f7a..1d614def9 100644
--- a/src/components/molecules/conceptLink/ConceptLink.tsx
+++ b/src/components/molecules/conceptLink/ConceptLink.tsx
@@ -1,8 +1,7 @@
 import { Popover } from "@/components/atoms/popover/Popover";
-import { ExternalLink } from "@/components/ExternalLink";
 import { TConcept } from "@/types";
 import { joinTailwindClasses } from "@/utils/tailwind";
-import Link from "next/link";
+import { startCase } from "lodash";
 import { useState } from "react";
 
 interface ConceptLinkProps {
@@ -15,17 +14,24 @@ export const ConceptLink = ({ concept, triggerClasses = "" }: ConceptLinkProps)
 
   const allTriggerClasses = joinTailwindClasses(
     "inline text-text-primary capitalize underline underline-offset-2 decoration-dotted cursor-help",
-    isOpen ? "decoration-text-primary" : "decoration-text-tertiary",
+    isOpen ? "decoration-text-primary" : "decoration-text-quaternary",
     triggerClasses
   );
 
+  const title = startCase(concept.preferred_label);
+
   return (
-    <Popover openOnHover onOpenChange={setIsOpen} trigger={<span className={allTriggerClasses}>{concept.preferred_label}</span>}>
-      <span className="font-bold capitalize underline-offset-un">{concept.preferred_label}</span>
-      <p className="my-2">{concept.description}</p>
-      <ExternalLink className="underline" url={`https://climatepolicyradar.wikibase.cloud/wiki/Item:${concept.wikibase_id}`}>
-        Source
-      </ExternalLink>
-    </Popover>
+    <Popover
+      openOnHover
+      onOpenChange={setIsOpen}
+      trigger={<span className={allTriggerClasses}>{title}</span>}
+      title={title}
+      description={concept.description}
+      link={{
+        href: `https://climatepolicyradar.wikibase.cloud/wiki/Item:${concept.wikibase_id}`,
+        text: "Source",
+        external: true,
+      }}
+    />
   );
 };
diff --git a/src/components/molecules/info/Info.stories.tsx b/src/components/molecules/info/Info.stories.tsx
index db8ec6c66..26b5c2eb6 100644
--- a/src/components/molecules/info/Info.stories.tsx
+++ b/src/components/molecules/info/Info.stories.tsx
@@ -5,39 +5,25 @@ const meta = {
   title: "Molecules/Info",
   component: Info,
   parameters: { layout: "centered" },
-  argTypes: {
-    children: { control: "text" },
-  },
+  argTypes: {},
 } satisfies Meta<typeof Info>;
 type Story = StoryObj<typeof Info>;
 
 export default meta;
 
-export const Generic: Story = {
+export const Filters: Story = {
   args: {
-    children: "Here is some more info!",
+    title: "About",
+    description: "Narrow down your results using the filter below. You can also combine these with a search term.",
+    link: {
+      href: "#",
+      text: "Learn more",
+    },
   },
 };
 
-export const Filters: Story = {
-  argTypes: {
-    children: { control: false },
-  },
+export const Minimal: Story = {
   args: {
-    children: (
-      <>
-        <span className="font-bold">About</span>
-        <p className="my-2">Narrow down your results using the filter below. You can also combine these with a search term.</p>
-        <a href="#" className="underline">
-          Learn more
-        </a>
-      </>
-    ),
+    description: "Here is some more info!",
   },
-  render: ({ ...props }) => (
-    <p className="flex gap-1.5 items-center">
-      <span className="font-medium">Filters</span>
-      <Info {...props} />
-    </p>
-  ),
 };
diff --git a/src/components/molecules/info/Info.tsx b/src/components/molecules/info/Info.tsx
index 6c25bddce..0597f20e9 100644
--- a/src/components/molecules/info/Info.tsx
+++ b/src/components/molecules/info/Info.tsx
@@ -1,14 +1,16 @@
-import { Popover } from "@/components/atoms/popover/Popover";
+import { Popover, TPopoverLink } from "@/components/atoms/popover/Popover";
 import { joinTailwindClasses } from "@/utils/tailwind";
-import { ReactNode, useState } from "react";
+import { useState } from "react";
 import { LuInfo } from "react-icons/lu";
 
-type InfoProps = {
+type TInfoProps = {
   className?: string;
-  children: ReactNode;
+  title?: string;
+  description: string;
+  link?: TPopoverLink;
 };
 
-export const Info = ({ className, children }: InfoProps) => {
+export const Info = ({ className, description, link, title }: TInfoProps) => {
   const [isOpen, setIsOpen] = useState(false);
 
   const infoClasses = joinTailwindClasses("cursor-help", isOpen ? "text-text-brand" : "text-text-secondary", className);
@@ -19,11 +21,12 @@ export const Info = ({ className, children }: InfoProps) => {
       onOpenChange={setIsOpen}
       trigger={
         <div className={infoClasses}>
-          <LuInfo height="16" width="16" />
+          <LuInfo size={16} />
         </div>
       }
-    >
-      {children}
-    </Popover>
+      title={title}
+      description={description}
+      link={link}
+    />
   );
 };
diff --git a/src/pages/documents/[id].tsx b/src/pages/documents/[id].tsx
index d0030c08e..ad196f5ab 100644
--- a/src/pages/documents/[id].tsx
+++ b/src/pages/documents/[id].tsx
@@ -2,7 +2,6 @@ import { useEffect, useState, useCallback, useMemo } from "react";
 import { GetServerSideProps, InferGetServerSidePropsType } from "next";
 import { useRouter } from "next/router";
 import { AnimatePresence, motion } from "framer-motion";
-import { LuSettings2 } from "react-icons/lu";
 
 import { ApiClient } from "@/api/http-common";
 
@@ -229,7 +228,7 @@ const DocumentPage: InferGetServerSidePropsType<typeof getServerSideProps> = ({
                         <div className="flex-1">
                           {totalNoOfMatches > 0 && (
                             <>
-                              <div className="mb-2 pt-2 text-sm" data-cy="document-matches-description">
+                              <div className="mb-2 text-sm" data-cy="document-matches-description">
                                 Displaying {renderPassageCount(totalNoOfMatches)} for "
                                 <span className="text-textDark font-medium">{`${qsSearchString}`}</span>"
                                 {!searchQuery.exact_match && ` and related phrases`}
@@ -244,8 +243,11 @@ const DocumentPage: InferGetServerSidePropsType<typeof getServerSideProps> = ({
                           )}
                         </div>
                         <div className="relative z-10 flex justify-center">
-                          <button className="p-2 text-textDark text-xl" onClick={() => setShowOptions(!showOptions)}>
-                            <LuSettings2 />
+                          <button
+                            className={`px-1 py-0.5 -mt-0.5 rounded-md text-sm text-text-primary font-normal ${showOptions ? "bg-surface-ui" : ""}`}
+                            onClick={() => setShowOptions(!showOptions)}
+                          >
+                            Sort &amp; Display
                           </button>
                           <AnimatePresence initial={false}>
                             {showOptions && (
diff --git a/src/pages/search/index.tsx b/src/pages/search/index.tsx
index ea56229ed..548a7e499 100644
--- a/src/pages/search/index.tsx
+++ b/src/pages/search/index.tsx
@@ -2,7 +2,6 @@ import { AnimatePresence, motion } from "framer-motion";
 import { GetServerSideProps, InferGetServerSidePropsType } from "next";
 import { useRouter } from "next/router";
 import { useEffect, useRef, useState } from "react";
-import { LuSettings2 } from "react-icons/lu";
 
 import { ApiClient } from "@/api/http-common";
 
@@ -16,7 +15,6 @@ import { SingleCol } from "@/components/panels/SingleCol";
 
 import { ExternalLink } from "@/components/ExternalLink";
 import Loader from "@/components/Loader";
-import { NoOfResults } from "@/components/NoOfResults";
 import SearchFilters from "@/components/blocks/SearchFilters";
 import { BreadCrumbs } from "@/components/breadcrumbs/Breadcrumbs";
 import Drawer from "@/components/drawer/Drawer";
@@ -43,6 +41,7 @@ import { SlideOutContext, TSlideOutContent } from "@/context/SlideOutContext";
 
 import { TConcept, TTheme, TThemeConfig } from "@/types";
 import { withEnvConfig } from "@/context/EnvConfig";
+import { Info } from "@/components/molecules/info/Info";
 
 type TProps = {
   theme: TTheme;
@@ -58,7 +57,6 @@ const SETTINGS_ANIMATION_VARIANTS = {
 
 const Search: InferGetServerSidePropsType<typeof getServerSideProps> = ({ theme, themeConfig, featureFlags, conceptsData }: TProps) => {
   const router = useRouter();
-  const qQueryString = router.query[QUERY_PARAMS.query_string];
   const [showFilters, setShowFilters] = useState(false);
   const [showCSVDownloadPopup, setShowCSVDownloadPopup] = useState(false);
   const [showOptions, setShowOptions] = useState(false);
@@ -440,20 +438,20 @@ const Search: InferGetServerSidePropsType<typeof getServerSideProps> = ({ theme,
               <SingleCol extraClasses="px-5 pt-5 relative">
                 <div>
                   <div className="">
-                    <div className="flex justify-between flex-wrap gap-2 items-center">
+                    <div className="flex justify-between flex-wrap gap-2 items-start">
                       <div className="md:hidden">
                         <Button content="both" className="flex-nowrap" onClick={toggleFilters}>
                           <span>{showFilters ? "Hide" : "Show"} filters</span>
                         </Button>
                       </div>
-                      <div className="relative z-10 md:order-1">
+                      <div className="relative z-10 -top-0.5 md:order-1">
                         <button
-                          className={`p-4 text-textDark text-xl ${showOptions ? "bg-nearBlack text-white rounded-full" : ""}`}
+                          className={`px-1 py-0.5 -mt-0.5 rounded-md text-sm text-text-primary font-normal ${showOptions ? "bg-surface-ui" : ""}`}
                           onClick={() => setShowOptions(!showOptions)}
                           data-cy="search-options"
                           ref={settingsButtonRef}
                         >
-                          <LuSettings2 />
+                          Sort &amp; Display
                         </button>
                         <AnimatePresence initial={false}>
                           {showOptions && (
@@ -469,8 +467,15 @@ const Search: InferGetServerSidePropsType<typeof getServerSideProps> = ({ theme,
                           )}
                         </AnimatePresence>
                       </div>
-                      <div className="text-xs basis-full md:basis-auto md:order-0" data-cy="number-of-results">
-                        {status === "success" && <NoOfResults hits={hits} queryString={qQueryString} />}
+                      <div className="flex items-center gap-2">
+                        <p className="text-sm text-text-primary font-normal">
+                          Results <span className="text-text-secondary">{hits || 0}</span>
+                        </p>
+                        <Info
+                          title="Showing the top 500 results"
+                          description="We limit the number of matches you can see so you get the quickest, most accurate results."
+                          link={{ href: "/faq", text: "Learn more" }}
+                        />
                       </div>
                     </div>
                   </div>
diff --git a/src/styles/styles.css b/src/styles/styles.css
index 1e7de6e0b..1a521a1c3 100644
--- a/src/styles/styles.css
+++ b/src/styles/styles.css
@@ -616,3 +616,9 @@ ul li.selected {
   height: 32px;
   background: hsl(200, 50%, 0%);
 }
+
+/* Base UI elements */
+[data-floating-ui-portal] {
+  position: absolute;
+  z-index: 1000;
+}
diff --git a/src/styles/theme.css b/src/styles/theme.css
index 0da796f8e..416870b2d 100644
--- a/src/styles/theme.css
+++ b/src/styles/theme.css
@@ -75,6 +75,7 @@
   --color-text-primary: #202020;
   --color-text-secondary: #505050;
   --color-text-tertiary: #707070;
+  --color-text-quaternary: #858585;
   --color-text-light: #ffffff;
   --color-text-brand: #005eeb;
 
diff --git a/src/utils/baseUI.tsx b/src/utils/baseUI.tsx
new file mode 100644
index 000000000..375d37dbf
--- /dev/null
+++ b/src/utils/baseUI.tsx
@@ -0,0 +1,12 @@
+export const BaseUIArrow = ({ fill }: { fill: string }) => (
+  <svg width="20" height="10" viewBox="0 0 20 10" fill="none">
+    <path
+      d="M9.66437 2.60207L4.80758 6.97318C4.07308 7.63423 3.11989 8 2.13172 8H0V10H20V8H18.5349C17.5468 8 16.5936 7.63423 15.8591 6.97318L11.0023 2.60207C10.622 2.2598 10.0447 2.25979 9.66437 2.60207Z"
+      className={fill}
+    />
+    <path
+      d="M10.3333 3.34539L5.47654 7.71648C4.55842 8.54279 3.36693 9 2.13172 9H0V8H2.13172C3.11989 8 4.07308 7.63423 4.80758 6.97318L9.66437 2.60207C10.0447 2.25979 10.622 2.2598 11.0023 2.60207L15.8591 6.97318C16.5936 7.63423 17.5468 8 18.5349 8H20V9H18.5349C17.2998 9 16.1083 8.54278 15.1901 7.71648L10.3333 3.34539Z"
+      className={fill}
+    />
+  </svg>
+);


Reviews:
user: login: PatFawbertMills
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-12T12:48:51Z
user: login: PatFawbertMills
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-12T12:50:09Z
user: login: PatFawbertMills
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-12T12:52:05Z
user: login: PatFawbertMills
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-12T12:52:37Z
user: login: PatFawbertMills
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-12T12:53:22Z
user: login: PatFawbertMills
body: Comments but non blocking
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-05-12T12:53:36Z
user: login: jesskelsall
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-12T13:04:07Z
user: login: jesskelsall
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-12T13:05:20Z
user: login: jesskelsall
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-12T16:14:11Z

Review comments:
diff_hunk: @@ -1,29 +1,42 @@
+import { ExternalLink } from "@/components/ExternalLink";
+import { BaseUIArrow } from "@/utils/baseUI";
 import { joinTailwindClasses } from "@/utils/tailwind";
 import { Popover as BasePopover } from "@base-ui-components/react/popover";
+import Link from "next/link";
 import { ReactElement, ReactNode } from "react";
 
-const PopoverArrow = () => (
-  <svg width="20" height="10" viewBox="0 0 20 10" fill="none">
-    <path
-      d="M9.66437 2.60207L4.80758 6.97318C4.07308 7.63423 3.11989 8 2.13172 8H0V10H20V8H18.5349C17.5468 8 16.5936 7.63423 15.8591 6.97318L11.0023 2.60207C10.622 2.2598 10.0447 2.25979 9.66437 2.60207Z"
-      className="fill-surface-light"
-    />
-    <path
-      d="M10.3333 3.34539L5.47654 7.71648C4.55842 8.54279 3.36693 9 2.13172 9H0V8H2.13172C3.11989 8 4.07308 7.63423 4.80758 6.97318L9.66437 2.60207C10.0447 2.25979 10.622 2.2598 11.0023 2.60207L15.8591 6.97318C16.5936 7.63423 17.5468 8 18.5349 8H20V9H18.5349C17.2998 9 16.1083 8.54278 15.1901 7.71648L10.3333 3.34539Z"
-      className="fill-border-light"
-    />
-  </svg>
-);
-
-interface PopoverProps {
-  children: ReactNode;
+export type TPopoverLink = {
+  href: string;
+  text: string;
+  external?: boolean;
+};
+
+// Provide options for structuring the popover using props & Base UI components (heavily preferred) or children for full flexibility
+
+interface PopoverGenericProps {
   onOpenChange?: (open: boolean) => void;
   openOnHover?: boolean;
   popupClasses?: string;
   trigger: ReactElement;
 }
 
-export const Popover = ({ children, onOpenChange, openOnHover = false, popupClasses = "", trigger }: PopoverProps) => {
+interface PopoverElementProps extends PopoverGenericProps {
+  title?: string;
+  description: string;
+  link?: TPopoverLink;
+  children?: never;
+}
+
+interface PopoverChildrenProps extends PopoverGenericProps {
+  children: ReactNode;
+  title?: never;
+  description?: never;
+  link?: never;
+}
+
+type PopoverProps = PopoverElementProps | PopoverChildrenProps;
path: src/components/atoms/popover/Popover.tsx
user: login: PatFawbertMills
body: Request: we decide between ourselves a naming convention for types and interfaces.

Personally I quite like the prefix approach so that one instantly knows what the nature of the thing is, e.g. `TTypeName`, `IInterfaceName`, or even `TProps` if it is only used in the component.
created_at: 2025-05-12T12:48:50Z
author_association: COLLABORATOR
diff_hunk: @@ -266,10 +266,13 @@ export const ConceptsDocumentViewer = ({
           )}`}
         >
           <div className="relative">
-            <div className="flex justify-between p-4">
+            <div className="flex justify-between items-end p-4">
               <h1 className="text-base font-medium">Passage matches</h1>
-              <button className="text-xl text-text-tertiary" onClick={() => setShowSearchOptions(!showSearchOptions)}>
-                <LuSettings2 />
+              <button
+                className={`px-1 py-0.5 -mt-0.5 rounded-md text-sm text-text-primary font-normal ${showSearchOptions ? "bg-surface-ui" : ""}`}
+                onClick={() => setShowSearchOptions(!showSearchOptions)}
+              >
+                Sort & Display
path: src/components/documents/ConceptsDocumentViewer.tsx
user: login: PatFawbertMills
body: Question: do we need to html encode the ampersand? I have (mostly) got in the habit of doing so, but genuinely curious if it is still necessary.
created_at: 2025-05-12T12:50:09Z
author_association: COLLABORATOR
diff_hunk: @@ -244,8 +243,11 @@ const DocumentPage: InferGetServerSidePropsType<typeof getServerSideProps> = ({
                           )}
                         </div>
                         <div className="relative z-10 flex justify-center">
-                          <button className="p-2 text-textDark text-xl" onClick={() => setShowOptions(!showOptions)}>
-                            <LuSettings2 />
+                          <button
+                            className={`px-1 py-0.5 -mt-0.5 rounded-md text-sm text-text-primary font-normal ${showOptions ? "bg-surface-ui" : ""}`}
+                            onClick={() => setShowOptions(!showOptions)}
+                          >
+                            Sort & Display
path: src/pages/documents/[id].tsx
user: login: PatFawbertMills
body: Same question RE: ampersand as above
created_at: 2025-05-12T12:52:05Z
author_association: COLLABORATOR
diff_hunk: @@ -440,20 +438,20 @@ const Search: InferGetServerSidePropsType<typeof getServerSideProps> = ({ theme,
               <SingleCol extraClasses="px-5 pt-5 relative">
                 <div>
                   <div className="">
-                    <div className="flex justify-between flex-wrap gap-2 items-center">
+                    <div className="flex justify-between flex-wrap gap-2 items-start">
                       <div className="md:hidden">
                         <Button content="both" className="flex-nowrap" onClick={toggleFilters}>
                           <span>{showFilters ? "Hide" : "Show"} filters</span>
                         </Button>
                       </div>
-                      <div className="relative z-10 md:order-1">
+                      <div className="relative z-10 -top-0.5 md:order-1">
                         <button
-                          className={`p-4 text-textDark text-xl ${showOptions ? "bg-nearBlack text-white rounded-full" : ""}`}
+                          className={`px-1 py-0.5 -mt-0.5 rounded-md text-sm text-text-primary font-normal ${showOptions ? "bg-surface-ui" : ""}`}
                           onClick={() => setShowOptions(!showOptions)}
                           data-cy="search-options"
                           ref={settingsButtonRef}
                         >
-                          <LuSettings2 />
+                          Sort & Display
path: src/pages/search/index.tsx
user: login: PatFawbertMills
body: Same question RE: ampersand as above
created_at: 2025-05-12T12:52:37Z
author_association: COLLABORATOR
diff_hunk: @@ -75,6 +75,7 @@
   --color-text-primary: #202020;
   --color-text-secondary: #505050;
   --color-text-tertiary: #707070;
+  --color-text-quaternary: #858585;
path: src/styles/theme.css
user: login: PatFawbertMills
body: TIL: the name for 4th of a thing
created_at: 2025-05-12T12:53:22Z
author_association: COLLABORATOR
diff_hunk: @@ -1,29 +1,42 @@
+import { ExternalLink } from "@/components/ExternalLink";
+import { BaseUIArrow } from "@/utils/baseUI";
 import { joinTailwindClasses } from "@/utils/tailwind";
 import { Popover as BasePopover } from "@base-ui-components/react/popover";
+import Link from "next/link";
 import { ReactElement, ReactNode } from "react";
 
-const PopoverArrow = () => (
-  <svg width="20" height="10" viewBox="0 0 20 10" fill="none">
-    <path
-      d="M9.66437 2.60207L4.80758 6.97318C4.07308 7.63423 3.11989 8 2.13172 8H0V10H20V8H18.5349C17.5468 8 16.5936 7.63423 15.8591 6.97318L11.0023 2.60207C10.622 2.2598 10.0447 2.25979 9.66437 2.60207Z"
-      className="fill-surface-light"
-    />
-    <path
-      d="M10.3333 3.34539L5.47654 7.71648C4.55842 8.54279 3.36693 9 2.13172 9H0V8H2.13172C3.11989 8 4.07308 7.63423 4.80758 6.97318L9.66437 2.60207C10.0447 2.25979 10.622 2.2598 11.0023 2.60207L15.8591 6.97318C16.5936 7.63423 17.5468 8 18.5349 8H20V9H18.5349C17.2998 9 16.1083 8.54278 15.1901 7.71648L10.3333 3.34539Z"
-      className="fill-border-light"
-    />
-  </svg>
-);
-
-interface PopoverProps {
-  children: ReactNode;
+export type TPopoverLink = {
+  href: string;
+  text: string;
+  external?: boolean;
+};
+
+// Provide options for structuring the popover using props & Base UI components (heavily preferred) or children for full flexibility
+
+interface PopoverGenericProps {
   onOpenChange?: (open: boolean) => void;
   openOnHover?: boolean;
   popupClasses?: string;
   trigger: ReactElement;
 }
 
-export const Popover = ({ children, onOpenChange, openOnHover = false, popupClasses = "", trigger }: PopoverProps) => {
+interface PopoverElementProps extends PopoverGenericProps {
+  title?: string;
+  description: string;
+  link?: TPopoverLink;
+  children?: never;
+}
+
+interface PopoverChildrenProps extends PopoverGenericProps {
+  children: ReactNode;
+  title?: never;
+  description?: never;
+  link?: never;
+}
+
+type PopoverProps = PopoverElementProps | PopoverChildrenProps;
path: src/components/atoms/popover/Popover.tsx
user: login: jesskelsall
body: My usage here is purely out of habit, I agree that it'd be best to standardise.

Personally I don't see a ton of value in using a prefix. There are rarely cases where the type/interface distinction is useful for TypeScript in React, and IDEs make it easy to contextualise the type on hover.

That being said, the prefix is useful at a glance (if a bit ugly), and I like that it adds a bit more intentionality to deciding whether to use a type or interface for an object.
created_at: 2025-05-12T13:04:07Z
author_association: CONTRIBUTOR
diff_hunk: @@ -266,10 +266,13 @@ export const ConceptsDocumentViewer = ({
           )}`}
         >
           <div className="relative">
-            <div className="flex justify-between p-4">
+            <div className="flex justify-between items-end p-4">
               <h1 className="text-base font-medium">Passage matches</h1>
-              <button className="text-xl text-text-tertiary" onClick={() => setShowSearchOptions(!showSearchOptions)}>
-                <LuSettings2 />
+              <button
+                className={`px-1 py-0.5 -mt-0.5 rounded-md text-sm text-text-primary font-normal ${showSearchOptions ? "bg-surface-ui" : ""}`}
+                onClick={() => setShowSearchOptions(!showSearchOptions)}
+              >
+                Sort & Display
path: src/components/documents/ConceptsDocumentViewer.tsx
user: login: jesskelsall
body: As far as I know it's not _necessary_ any more but using it provides more protection. Will change.
created_at: 2025-05-12T13:05:20Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,29 +1,42 @@
+import { ExternalLink } from "@/components/ExternalLink";
+import { BaseUIArrow } from "@/utils/baseUI";
 import { joinTailwindClasses } from "@/utils/tailwind";
 import { Popover as BasePopover } from "@base-ui-components/react/popover";
+import Link from "next/link";
 import { ReactElement, ReactNode } from "react";
 
-const PopoverArrow = () => (
-  <svg width="20" height="10" viewBox="0 0 20 10" fill="none">
-    <path
-      d="M9.66437 2.60207L4.80758 6.97318C4.07308 7.63423 3.11989 8 2.13172 8H0V10H20V8H18.5349C17.5468 8 16.5936 7.63423 15.8591 6.97318L11.0023 2.60207C10.622 2.2598 10.0447 2.25979 9.66437 2.60207Z"
-      className="fill-surface-light"
-    />
-    <path
-      d="M10.3333 3.34539L5.47654 7.71648C4.55842 8.54279 3.36693 9 2.13172 9H0V8H2.13172C3.11989 8 4.07308 7.63423 4.80758 6.97318L9.66437 2.60207C10.0447 2.25979 10.622 2.2598 11.0023 2.60207L15.8591 6.97318C16.5936 7.63423 17.5468 8 18.5349 8H20V9H18.5349C17.2998 9 16.1083 8.54278 15.1901 7.71648L10.3333 3.34539Z"
-      className="fill-border-light"
-    />
-  </svg>
-);
-
-interface PopoverProps {
-  children: ReactNode;
+export type TPopoverLink = {
+  href: string;
+  text: string;
+  external?: boolean;
+};
+
+// Provide options for structuring the popover using props & Base UI components (heavily preferred) or children for full flexibility
+
+interface PopoverGenericProps {
   onOpenChange?: (open: boolean) => void;
   openOnHover?: boolean;
   popupClasses?: string;
   trigger: ReactElement;
 }
 
-export const Popover = ({ children, onOpenChange, openOnHover = false, popupClasses = "", trigger }: PopoverProps) => {
+interface PopoverElementProps extends PopoverGenericProps {
+  title?: string;
+  description: string;
+  link?: TPopoverLink;
+  children?: never;
+}
+
+interface PopoverChildrenProps extends PopoverGenericProps {
+  children: ReactNode;
+  title?: never;
+  description?: never;
+  link?: never;
+}
+
+type PopoverProps = PopoverElementProps | PopoverChildrenProps;
path: src/components/atoms/popover/Popover.tsx
user: login: jesskelsall
body: Outcome: We have decided to use `T` and `I` variable prefixes consistently.
created_at: 2025-05-12T16:14:10Z
author_association: CONTRIBUTOR

PR comments:
user: login: linear[bot]
created_at: 2025-05-12T09:56:21Z
body: <p><a href="https://linear.app/climate-policy-radar/issue/APP-541/labelling">APP-541 Labelling</a></p>
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-05-12T09:58:58Z
body: ## size-limit report 📦 
| Path              | Size                  |
| ----------------- | --------------------- |
| Next static files | 343.85 KB (+1.34% 🔺) |
author_association: NONE
user: login: trunk-io[bot]
created_at: 2025-05-12T10:35:39Z
body: <!-- Trunk Test Analytics -->


[![Static Badge](https://raster.shields.io/badge/29_tests-10742f54-purple)](https://app.trunk.io/climatepolicyradar/flaky-tests/pr/510?repo=climatepolicyradar/navigator-frontend&commitHash=10742f54f09950544cc84f3c5a9fd83dd796d548&utm_source=gh-comment) &ensp; [![Static Badge](https://raster.shields.io/badge/0-failed-crimson)](https://app.trunk.io/climatepolicyradar/flaky-tests/pr/510?repo=climatepolicyradar/navigator-frontend&commitHash=10742f54f09950544cc84f3c5a9fd83dd796d548&utm_source=gh-comment&runConclusion=FAILURE) &ensp; [![Static Badge](https://raster.shields.io/badge/0-flaky-red)](https://app.trunk.io/climatepolicyradar/flaky-tests/pr/510?repo=climatepolicyradar/navigator-frontend&commitHash=10742f54f09950544cc84f3c5a9fd83dd796d548&utm_source=gh-comment&runConclusion=FAILURE) &ensp; [![Static Badge](https://raster.shields.io/badge/0-quarantined-yellow)](https://app.trunk.io/climatepolicyradar/flaky-tests/pr/510?repo=climatepolicyradar/navigator-frontend&commitHash=10742f54f09950544cc84f3c5a9fd83dd796d548&utm_source=gh-comment&runConclusion=QUARANTINED)


<sub>

[View Full Report ↗︎](https://app.trunk.io/climatepolicyradar/flaky-tests/pr/510?repo=climatepolicyradar/navigator-frontend&commitHash=10742f54f09950544cc84f3c5a9fd83dd796d548&utm_source=gh-comment) ⋅ [Docs](https://docs.trunk.io/flaky-tests?utm_source=gh-comment)

</sub>
author_association: NONE
user: login: PatFawbertMills
created_at: 2025-05-12T11:12:10Z
body: I was about to say "we do limit the results" but I prefer this way of presenting the data. Nice.
author_association: COLLABORATOR