From 34ca45e98534af23eae871b0a61662ff2e360411 Mon Sep 17 00:00:00 2001
From: Dave Falke <dfalke@uga.edu>
Date: Wed, 20 Nov 2024 09:36:16 -0500
Subject: [PATCH 1/4] checkpoint

---
 packages/libs/eda/src/index.tsx               | 12 +++
 .../lib/core/components/FilterChipList.tsx    |  2 +-
 .../src/lib/notebook/EdaNotebookAnalysis.tsx  | 86 +++++++++++++++++++
 .../lib/notebook/EdaNotebookLandingPage.tsx   | 37 ++++++++
 .../eda/src/lib/notebook/NotebookRoute.tsx    | 64 ++++++++++++++
 5 files changed, 200 insertions(+), 1 deletion(-)
 create mode 100644 packages/libs/eda/src/lib/notebook/EdaNotebookAnalysis.tsx
 create mode 100644 packages/libs/eda/src/lib/notebook/EdaNotebookLandingPage.tsx
 create mode 100644 packages/libs/eda/src/lib/notebook/NotebookRoute.tsx

diff --git a/packages/libs/eda/src/index.tsx b/packages/libs/eda/src/index.tsx
index de7c8ec358..6432296ca0 100644
--- a/packages/libs/eda/src/index.tsx
+++ b/packages/libs/eda/src/index.tsx
@@ -60,6 +60,7 @@ import './index.css';
 
 // snackbar
 import makeSnackbarProvider from '@veupathdb/coreui/lib/components/notifications/SnackbarProvider';
+import NotebookRoute from './lib/notebook/NotebookRoute';
 
 // Set singleAppMode to the name of one app, if the eda should use one instance of one app only.
 // Otherwise, let singleAppMode remain undefined or set it to '' to allow multiple app instances.
@@ -169,9 +170,20 @@ initialize({
               <Link to="/maps/studies">All studies</Link>
             </li>
           </ul>
+          <h3>Notebook Links</h3>
+          <ul>
+            <li>
+              <Link to="/notebook">All notebooks</Link>
+            </li>
+          </ul>
         </div>
       ),
     },
+    {
+      path: '/notebook',
+      exact: false,
+      component: () => <NotebookRoute edaServiceUrl={edaEndpoint} />,
+    },
     {
       path: '/eda',
       exact: false,
diff --git a/packages/libs/eda/src/lib/core/components/FilterChipList.tsx b/packages/libs/eda/src/lib/core/components/FilterChipList.tsx
index 7d3bbef119..326b6151b6 100644
--- a/packages/libs/eda/src/lib/core/components/FilterChipList.tsx
+++ b/packages/libs/eda/src/lib/core/components/FilterChipList.tsx
@@ -10,7 +10,7 @@ import { colors, Warning } from '@veupathdb/coreui';
 // Material UI CSS declarations
 const useStyles = makeStyles((theme) => ({
   chips: {
-    display: 'flex',
+    display: 'inline-flex',
     flexWrap: 'wrap',
     '& > *:not(:last-of-type)': {
       // Spacing between chips
diff --git a/packages/libs/eda/src/lib/notebook/EdaNotebookAnalysis.tsx b/packages/libs/eda/src/lib/notebook/EdaNotebookAnalysis.tsx
new file mode 100644
index 0000000000..42dd05d224
--- /dev/null
+++ b/packages/libs/eda/src/lib/notebook/EdaNotebookAnalysis.tsx
@@ -0,0 +1,86 @@
+import React, { useState } from 'react';
+import {
+  Filter,
+  useAnalysis,
+  useStudyEntities,
+  useStudyMetadata,
+  useStudyRecord,
+} from '../core';
+import { safeHtml } from '@veupathdb/wdk-client/lib/Utils/ComponentUtils';
+import { SaveableTextEditor } from '@veupathdb/wdk-client/lib/Components';
+import Subsetting from '../workspace/Subsetting';
+import { useEntityCounts } from '../core/hooks/entityCounts';
+import FilterChipList from '../core/components/FilterChipList';
+
+interface Props {
+  analysisId: string;
+}
+
+export function EdaNotebookAnalysis(props: Props) {
+  const { analysisId } = props;
+  const analysisState = useAnalysis(
+    analysisId === 'new' ? undefined : analysisId
+  );
+  const studyRecord = useStudyRecord();
+  const studyMetadata = useStudyMetadata();
+  const entities = useStudyEntities();
+  const totalCountsResult = useEntityCounts();
+  const filteredCountsResult = useEntityCounts(
+    analysisState.analysis?.descriptor.subset.descriptor
+  );
+  const [entityId, setEntityId] = useState<string>();
+  const [variableId, setVariableId] = useState<string>();
+  return (
+    <div>
+      <h1>EDA Notebook</h1>
+      {safeHtml(studyRecord.displayName, null, 'h2')}
+      <h3>
+        <SaveableTextEditor
+          value={analysisState.analysis?.displayName ?? ''}
+          onSave={analysisState.setName}
+        />
+      </h3>
+      <details>
+        <summary>
+          Subset &nbsp;&nbsp;
+          <FilterChipList
+            filters={analysisState.analysis?.descriptor.subset.descriptor}
+            entities={entities}
+            selectedEntityId={entityId}
+            selectedVariableId={variableId}
+            removeFilter={(filter) =>
+              analysisState.setFilters((filters) =>
+                filters.filter(
+                  (f) =>
+                    f.entityId !== filter.entityId ||
+                    f.variableId !== filter.variableId
+                )
+              )
+            }
+            variableLinkConfig={{
+              type: 'button',
+              onClick: (value) => {
+                setEntityId(value?.entityId);
+                setVariableId(value?.variableId);
+              },
+            }}
+          />
+        </summary>
+        <Subsetting
+          analysisState={analysisState}
+          entityId={entityId ?? ''}
+          variableId={variableId ?? ''}
+          totalCounts={totalCountsResult.value}
+          filteredCounts={filteredCountsResult.value}
+          variableLinkConfig={{
+            type: 'button',
+            onClick: (value) => {
+              setEntityId(value?.entityId);
+              setVariableId(value?.variableId);
+            },
+          }}
+        />
+      </details>
+    </div>
+  );
+}
diff --git a/packages/libs/eda/src/lib/notebook/EdaNotebookLandingPage.tsx b/packages/libs/eda/src/lib/notebook/EdaNotebookLandingPage.tsx
new file mode 100644
index 0000000000..bd1b86c167
--- /dev/null
+++ b/packages/libs/eda/src/lib/notebook/EdaNotebookLandingPage.tsx
@@ -0,0 +1,37 @@
+import React from 'react';
+import { useWdkStudyRecords } from '../core/hooks/study';
+import { useConfiguredSubsettingClient } from '../core/hooks/client';
+import { Link, useRouteMatch } from 'react-router-dom';
+import { safeHtml } from '@veupathdb/wdk-client/lib/Utils/ComponentUtils';
+
+interface Props {
+  edaServiceUrl: string;
+}
+
+export function EdaNotebookLandingPage(props: Props) {
+  const subsettingClient = useConfiguredSubsettingClient(props.edaServiceUrl);
+  const datasets = useWdkStudyRecords(subsettingClient);
+  const { url } = useRouteMatch();
+  return (
+    <div>
+      <h1>EDA Notebooks</h1>
+      <div>
+        <h2>Start a new notebook</h2>
+        <ul>
+          {datasets?.map((dataset) => (
+            <li>
+              {safeHtml(
+                dataset.displayName,
+                { to: `${url}/${dataset.attributes.dataset_id as string}/new` },
+                Link
+              )}
+            </li>
+          ))}
+        </ul>
+      </div>
+      <hr />
+      <div>MY NOTEBOOKS</div>
+      <div>SHARED NOTEBOOKS</div>
+    </div>
+  );
+}
diff --git a/packages/libs/eda/src/lib/notebook/NotebookRoute.tsx b/packages/libs/eda/src/lib/notebook/NotebookRoute.tsx
new file mode 100644
index 0000000000..cec69d1b8f
--- /dev/null
+++ b/packages/libs/eda/src/lib/notebook/NotebookRoute.tsx
@@ -0,0 +1,64 @@
+import React, { ComponentType } from 'react';
+import { Route, Switch, useRouteMatch } from 'react-router-dom';
+import { EdaNotebookLandingPage } from './EdaNotebookLandingPage';
+import { EdaNotebookAnalysis } from './EdaNotebookAnalysis';
+import {
+  EDAWorkspaceContainer,
+  useConfiguredAnalysisClient,
+  useConfiguredComputeClient,
+  useConfiguredDataClient,
+  useConfiguredDownloadClient,
+  useConfiguredSubsettingClient,
+} from '../core';
+import { DocumentationContainer } from '../core/components/docs/DocumentationContainer';
+import { QueryClientProvider } from '@tanstack/react-query';
+import { queryClient } from '../core/api/queryClient';
+
+interface Props {
+  edaServiceUrl: string;
+  datasetId?: string;
+  analysisId?: string;
+}
+
+export default function NotebookRoute(props: Props) {
+  const { edaServiceUrl } = props;
+  const match = useRouteMatch();
+  const analysisClient = useConfiguredAnalysisClient(edaServiceUrl);
+  const subsettingClient = useConfiguredSubsettingClient(edaServiceUrl);
+  const downloadClient = useConfiguredDownloadClient(edaServiceUrl);
+  const dataClient = useConfiguredDataClient(edaServiceUrl);
+  const computeClient = useConfiguredComputeClient(edaServiceUrl);
+
+  return (
+    <DocumentationContainer>
+      <QueryClientProvider client={queryClient}>
+        <Switch>
+          <Route
+            exact
+            path={match.path}
+            render={() => (
+              <EdaNotebookLandingPage edaServiceUrl={edaServiceUrl} />
+            )}
+          />
+          <Route
+            path={`${match.path}/:datasetId/:analysisId`}
+            render={(props) => (
+              <EDAWorkspaceContainer
+                studyId={props.match.params.datasetId}
+                analysisClient={analysisClient}
+                subsettingClient={subsettingClient}
+                downloadClient={downloadClient}
+                dataClient={dataClient}
+                computeClient={computeClient}
+              >
+                <EdaNotebookAnalysis
+                  analysisId={props.match.params.analysisId}
+                />
+              </EDAWorkspaceContainer>
+            )}
+          />
+        </Switch>
+      </QueryClientProvider>
+    </DocumentationContainer>
+  );
+}

From e162ad7caa8ec5c9a6b76738b04df866f7ff08c8 Mon Sep 17 00:00:00 2001
From: Dave Falke <dfalke@uga.edu>
Date: Thu, 21 Nov 2024 14:07:26 -0500
Subject: [PATCH 2/4] Checkpoint

- Basics of cell types
- Persistence
- Iterate on styling
---
 .../src/lib/core/components/VariableLink.tsx  |   2 +-
 .../eda/src/lib/notebook/EdaNotebook.scss     |  35 ++++
 .../src/lib/notebook/EdaNotebookAnalysis.tsx  | 153 ++++++++++--------
 .../eda/src/lib/notebook/NotebookCell.tsx     |  28 ++++
 .../lib/notebook/SubsettingNotebookCell.tsx   |  58 +++++++
 packages/libs/eda/src/lib/notebook/Types.ts   |  44 +++++
 6 files changed, 250 insertions(+), 70 deletions(-)
 create mode 100644 packages/libs/eda/src/lib/notebook/EdaNotebook.scss
 create mode 100644 packages/libs/eda/src/lib/notebook/NotebookCell.tsx
 create mode 100644 packages/libs/eda/src/lib/notebook/SubsettingNotebookCell.tsx
 create mode 100644 packages/libs/eda/src/lib/notebook/Types.ts

diff --git a/packages/libs/eda/src/lib/core/components/VariableLink.tsx b/packages/libs/eda/src/lib/core/components/VariableLink.tsx
index 02fd7d9c29..f1625ce260 100644
--- a/packages/libs/eda/src/lib/core/components/VariableLink.tsx
+++ b/packages/libs/eda/src/lib/core/components/VariableLink.tsx
@@ -70,12 +70,12 @@ export const VariableLink = forwardRef(
         tabIndex={0}
         style={finalStyle}
         onKeyDown={(event) => {
-          event.preventDefault();
           if (disabled) {
             return;
           }
           if (event.key === 'Enter' || event.key === ' ') {
             linkConfig.onClick(value);
+            event.preventDefault();
           }
         }}
         onClick={(event) => {
diff --git a/packages/libs/eda/src/lib/notebook/EdaNotebook.scss b/packages/libs/eda/src/lib/notebook/EdaNotebook.scss
new file mode 100644
index 0000000000..0f0a50c61b
--- /dev/null
+++ b/packages/libs/eda/src/lib/notebook/EdaNotebook.scss
@@ -0,0 +1,35 @@
+.EdaNotebook {
+  .Heading {
+    display: flex;
+    gap: 2em;
+    align-items: baseline;
+  }
+
+  .Paper {
+    max-width: 1250px;
+    padding: 1em;
+    margin: 1em auto;
+    background-color: #f3f3f3;
+    box-shadow: 0 0 2px #b5b5b5;
+
+    > * + * {
+      margin-block-start: 1rem;
+    }
+    h2,
+    h3 {
+      padding: 0;
+    }
+    h3 {
+      font-size: 1em;
+      font-weight: 400;
+      line-height: 1.5;
+    }
+  }
+
+  .Title {
+    fieldset {
+      padding: 0;
+      margin: 0;
+    }
+  }
+}
diff --git a/packages/libs/eda/src/lib/notebook/EdaNotebookAnalysis.tsx b/packages/libs/eda/src/lib/notebook/EdaNotebookAnalysis.tsx
index 42dd05d224..327a3a7a20 100644
--- a/packages/libs/eda/src/lib/notebook/EdaNotebookAnalysis.tsx
+++ b/packages/libs/eda/src/lib/notebook/EdaNotebookAnalysis.tsx
@@ -1,16 +1,31 @@
-import React, { useState } from 'react';
-import {
-  Filter,
-  useAnalysis,
-  useStudyEntities,
-  useStudyMetadata,
-  useStudyRecord,
-} from '../core';
+// Notes
+// =====
+//
+// - For now, we will only support "fixed" notebooks. If we want to allow "custom" notebooks,
+//   we have to make some decisions.
+// - Do we want a top-down data flow? E.g., subsetting is global for an analysis.
+// - Do we want to separate compute config from visualization? If so, how do we
+//   support that in the UI?
+// - Do we want text-based cells?
+// - Do we want download cells? It could have a preview.
+//
+
+import React, { useCallback, useMemo } from 'react';
+import { useAnalysis, useStudyRecord } from '../core';
 import { safeHtml } from '@veupathdb/wdk-client/lib/Utils/ComponentUtils';
 import { SaveableTextEditor } from '@veupathdb/wdk-client/lib/Components';
-import Subsetting from '../workspace/Subsetting';
-import { useEntityCounts } from '../core/hooks/entityCounts';
-import FilterChipList from '../core/components/FilterChipList';
+import { ExpandablePanel } from '@veupathdb/coreui';
+import { NotebookCell as NotebookCellType } from './Types';
+import { NotebookCell } from './NotebookCell';
+
+import './EdaNotebook.scss';
+
+interface NotebookSettings {
+  /** Ordered array of notebook cells */
+  cells: NotebookCellType[];
+}
+
+const NOTEBOOK_UI_SETTINGS_KEY = '@@NOTEBOOK@@';
 
 interface Props {
   analysisId: string;
@@ -18,69 +33,69 @@ interface Props {
 
 export function EdaNotebookAnalysis(props: Props) {
   const { analysisId } = props;
+  const studyRecord = useStudyRecord();
   const analysisState = useAnalysis(
     analysisId === 'new' ? undefined : analysisId
   );
-  const studyRecord = useStudyRecord();
-  const studyMetadata = useStudyMetadata();
-  const entities = useStudyEntities();
-  const totalCountsResult = useEntityCounts();
-  const filteredCountsResult = useEntityCounts(
-    analysisState.analysis?.descriptor.subset.descriptor
+  const { analysis } = analysisState;
+  const notebookSettings = useMemo((): NotebookSettings => {
+    const storedSettings =
+      analysis?.descriptor.subset.uiSettings[NOTEBOOK_UI_SETTINGS_KEY];
+    if (storedSettings == null)
+      return {
+        cells: [
+          {
+            type: 'subset',
+            title: 'Subset data',
+          },
+        ],
+      };
+    return storedSettings as any as NotebookSettings;
+  }, [analysis]);
+  const updateCell = useCallback(
+    (cell: Partial<Omit<NotebookCellType, 'type'>>, cellIndex: number) => {
+      const oldCell = notebookSettings.cells[cellIndex];
+      const newCell = { ...oldCell, ...cell };
+      const nextCells = notebookSettings.cells.concat();
+      nextCells[cellIndex] = newCell;
+      const nextSettings = {
+        ...notebookSettings,
+        cells: nextCells,
+      };
+      analysisState.setVariableUISettings({
+        [NOTEBOOK_UI_SETTINGS_KEY]: nextSettings,
+      });
+    },
+    [analysisState, notebookSettings]
   );
-  const [entityId, setEntityId] = useState<string>();
-  const [variableId, setVariableId] = useState<string>();
   return (
-    <div>
-      <h1>EDA Notebook</h1>
-      {safeHtml(studyRecord.displayName, null, 'h2')}
-      <h3>
-        <SaveableTextEditor
-          value={analysisState.analysis?.displayName ?? ''}
-          onSave={analysisState.setName}
-        />
-      </h3>
-      <details>
-        <summary>
-          Subset &nbsp;&nbsp;
-          <FilterChipList
-            filters={analysisState.analysis?.descriptor.subset.descriptor}
-            entities={entities}
-            selectedEntityId={entityId}
-            selectedVariableId={variableId}
-            removeFilter={(filter) =>
-              analysisState.setFilters((filters) =>
-                filters.filter(
-                  (f) =>
-                    f.entityId !== filter.entityId ||
-                    f.variableId !== filter.variableId
-                )
-              )
-            }
-            variableLinkConfig={{
-              type: 'button',
-              onClick: (value) => {
-                setEntityId(value?.entityId);
-                setVariableId(value?.variableId);
-              },
-            }}
-          />
-        </summary>
-        <Subsetting
-          analysisState={analysisState}
-          entityId={entityId ?? ''}
-          variableId={variableId ?? ''}
-          totalCounts={totalCountsResult.value}
-          filteredCounts={filteredCountsResult.value}
-          variableLinkConfig={{
-            type: 'button',
-            onClick: (value) => {
-              setEntityId(value?.entityId);
-              setVariableId(value?.variableId);
-            },
-          }}
-        />
-      </details>
+    <div className="EdaNotebook">
+      <div className="Heading">
+        <h1>EDA Notebook</h1>
+      </div>
+      <div className="Paper">
+        <div>
+          <h2>
+            <SaveableTextEditor
+              className="Title"
+              value={analysisState.analysis?.displayName ?? ''}
+              onSave={analysisState.setName}
+            />
+          </h2>
+          <h3>Study: {safeHtml(studyRecord.displayName)}</h3>
+        </div>
+        {notebookSettings.cells.map((cell, index) => (
+          <ExpandablePanel title={cell.title} subTitle={{}} themeRole="primary">
+            <div style={{ padding: '1em' }}>
+              <NotebookCell
+                analysisState={analysisState}
+                cell={cell}
+                updateCell={(update) => updateCell(update, index)}
+              />
+            </div>
+          </ExpandablePanel>
+        ))}
+      </div>
     </div>
   );
 }
diff --git a/packages/libs/eda/src/lib/notebook/NotebookCell.tsx b/packages/libs/eda/src/lib/notebook/NotebookCell.tsx
new file mode 100644
index 0000000000..dbf36328e3
--- /dev/null
+++ b/packages/libs/eda/src/lib/notebook/NotebookCell.tsx
@@ -0,0 +1,28 @@
+import { AnalysisState } from '../core';
+import { NotebookCell as NotebookCellType } from './Types';
+import { SubsettingNotebookCell } from './SubsettingNotebookCell';
+
+interface Props {
+  analysisState: AnalysisState;
+  cell: NotebookCellType;
+  updateCell: (cell: Partial<Omit<NotebookCellType, 'type'>>) => void;
+}
+
+/**
+ * Top-level component that delegates to imeplementations of NotebookCell variants.
+ */
+export function NotebookCell(props: Props) {
+  const { cell, analysisState, updateCell } = props;
+  switch (cell.type) {
+    case 'subset':
+      return (
+        <SubsettingNotebookCell
+          cell={cell}
+          analysisState={analysisState}
+          updateCell={updateCell}
+        />
+      );
+    default:
+      return null;
+  }
+}
diff --git a/packages/libs/eda/src/lib/notebook/SubsettingNotebookCell.tsx b/packages/libs/eda/src/lib/notebook/SubsettingNotebookCell.tsx
new file mode 100644
index 0000000000..ef817993a1
--- /dev/null
+++ b/packages/libs/eda/src/lib/notebook/SubsettingNotebookCell.tsx
@@ -0,0 +1,58 @@
+import { useMemo } from 'react';
+import { useEntityCounts } from '../core/hooks/entityCounts';
+import { useStudyEntities } from '../core/hooks/workspace';
+import { NotebookCellComponentProps } from './Types';
+import { VariableLinkConfig } from '../core/components/VariableLink';
+import FilterChipList from '../core/components/FilterChipList';
+import Subsetting from '../workspace/Subsetting';
+
+export function SubsettingNotebookCell(
+  props: NotebookCellComponentProps<'subset'>
+) {
+  const { analysisState, cell, updateCell } = props;
+  const { selectedVariable } = cell;
+  const entities = useStudyEntities();
+  const totalCountsResult = useEntityCounts();
+  const filteredCountsResult = useEntityCounts(
+    analysisState.analysis?.descriptor.subset.descriptor
+  );
+  const variableLinkConfig = useMemo(
+    (): VariableLinkConfig => ({
+      type: 'button',
+      onClick: (selectedVariable) => {
+        updateCell({ selectedVariable });
+      },
+    }),
+    [updateCell]
+  );
+  return (
+    <div>
+      <div>
+        <FilterChipList
+          filters={analysisState.analysis?.descriptor.subset.descriptor}
+          entities={entities}
+          selectedEntityId={selectedVariable?.entityId}
+          selectedVariableId={selectedVariable?.variableId}
+          removeFilter={(filter) => {
+            analysisState.setFilters((filters) =>
+              filters.filter(
+                (f) =>
+                  f.entityId !== filter.entityId ||
+                  f.variableId !== filter.variableId
+              )
+            );
+          }}
+          variableLinkConfig={variableLinkConfig}
+        />
+      </div>
+      <Subsetting
+        analysisState={analysisState}
+        entityId={selectedVariable?.entityId ?? ''}
+        variableId={selectedVariable?.variableId ?? ''}
+        totalCounts={totalCountsResult.value}
+        filteredCounts={filteredCountsResult.value}
+        variableLinkConfig={variableLinkConfig}
+      />
+    </div>
+  );
+}
diff --git a/packages/libs/eda/src/lib/notebook/Types.ts b/packages/libs/eda/src/lib/notebook/Types.ts
new file mode 100644
index 0000000000..9575556c55
--- /dev/null
+++ b/packages/libs/eda/src/lib/notebook/Types.ts
@@ -0,0 +1,44 @@
+import { AnalysisState } from '../core/hooks/analysis';
+import { VariableDescriptor } from '../core/types/variable';
+
+export interface NotebookCellBase<T extends string> {
+  type: T;
+  title: string;
+}
+
+export interface SubsettingNotebookCell extends NotebookCellBase<'subset'> {
+  selectedVariable?: Partial<VariableDescriptor>;
+}
+
+export interface ComputeNotebookCell extends NotebookCellBase<'compute'> {
+  computeId: string;
+}
+
+export interface VisualizationNotebookCell
+  extends NotebookCellBase<'visualization'> {
+  visualizationId: string;
+}
+
+export interface TextNotebookCell extends NotebookCellBase<'text'> {
+  text: string;
+}
+
+export type NotebookCell =
+  | SubsettingNotebookCell
+  | ComputeNotebookCell
+  | VisualizationNotebookCell
+  | TextNotebookCell;
+
+type FindByType<Union, Type> = Union extends { type: Type } ? Union : never;
+
+export type NotebookCellOfType<T extends NotebookCell['type']> = FindByType<
+  NotebookCell,
+  T
+>;
+
+export interface NotebookCellComponentProps<T extends NotebookCell['type']> {
+  analysisState: AnalysisState;
+  cell: NotebookCellOfType<T>;
+  // Allow partial updates, but don't allow `type` to be changed.
+  updateCell: (cell: Omit<Partial<NotebookCellOfType<T>>, 'type'>) => void;
+}

From d1db6f97101662d1686f4be7bc14d6692a8aaba1 Mon Sep 17 00:00:00 2001
From: Dave Falke <dfalke@uga.edu>
Date: Fri, 22 Nov 2024 12:19:24 -0500
Subject: [PATCH 3/4] Checkpoint

- Expose some coreui theme values as custom css properties
- Replace ExpandablePanel with <details>
- Replace SCSS file with CSS file
---
 .../components/theming/UIThemeProvider.tsx    |  14 ++-
 .../libs/eda/src/lib/notebook/EdaNotebook.css | 116 ++++++++++++++++++
 .../eda/src/lib/notebook/EdaNotebook.scss     |  35 ------
 .../src/lib/notebook/EdaNotebookAnalysis.tsx  |  30 ++---
 4 files changed, 141 insertions(+), 54 deletions(-)
 create mode 100644 packages/libs/eda/src/lib/notebook/EdaNotebook.css
 delete mode 100644 packages/libs/eda/src/lib/notebook/EdaNotebook.scss

diff --git a/packages/libs/coreui/src/components/theming/UIThemeProvider.tsx b/packages/libs/coreui/src/components/theming/UIThemeProvider.tsx
index e6481d115a..b6c35d226c 100644
--- a/packages/libs/coreui/src/components/theming/UIThemeProvider.tsx
+++ b/packages/libs/coreui/src/components/theming/UIThemeProvider.tsx
@@ -14,13 +14,23 @@ export default function UIThemeProvider({
   children,
 }: UIThemeProviderProps) {
   useCoreUIFonts();
+  // In addition to making the theme available via React Context,
+  // we will also expose the theme as custom CSS properties.
   return (
     <ThemeProvider theme={theme}>
       <Global
         styles={css`
+          :root {
+            --coreui-color-primary: ${theme.palette.primary.hue[
+              theme.palette.primary.level
+            ]};
+            --coreui-color-secondary: ${theme.palette.secondary.hue[
+              theme.palette.secondary.level
+            ]};
+          }
+
           *:focus-visible {
-            outline: 2px solid
-              ${theme.palette.primary.hue[theme.palette.primary.level]};
+            outline: 2px solid var(--coreui-color-primary);
           }
         `}
       />
diff --git a/packages/libs/eda/src/lib/notebook/EdaNotebook.css b/packages/libs/eda/src/lib/notebook/EdaNotebook.css
new file mode 100644
index 0000000000..be2665e7d6
--- /dev/null
+++ b/packages/libs/eda/src/lib/notebook/EdaNotebook.css
@@ -0,0 +1,116 @@
+.EdaNotebook {
+  .Heading {
+    display: flex;
+    gap: 2em;
+    align-items: baseline;
+  }
+
+  .Paper {
+    /* A4 dimensions */
+    --paper-width: 2480px;
+    --paper-height: 3508px;
+    --paper-scale: 0.5;
+
+    width: calc(var(--paper-width) * var(--paper-scale));
+    /* height: calc(var(--paper-height) * var(--paper-scale)); */
+
+    padding: 2em;
+    margin: 1em auto;
+
+    /* background-color: #f3f3f3; */
+    box-shadow: 0 0 2px #b5b5b5;
+
+    > * + * {
+      margin-block-start: 1rem;
+    }
+
+    .Heading {
+      display: flex;
+      flex-direction: row;
+      justify-content: space-between;
+      gap: 0.5em;
+
+      h1 {
+        padding: 0;
+        font-size: 1.75em;
+      }
+
+      h2 {
+        font-size: 1em;
+        font-weight: bold;
+        padding: 0.25em 0.5em;
+        color: var(--coreui-color-primary, black);
+        border: 2px solid;
+        border-radius: 0.25em;
+        background-color: color-mix(
+          in srgb,
+          var(--coreui-color-primary) 5%,
+          transparent
+        );
+      }
+    }
+
+    > details {
+      border: 1px solid;
+      border-color: color-mix(
+        in srgb,
+        var(--coreui-color-primary) 30%,
+        transparent
+      );
+      border-top-left-radius: 0.5em;
+      border-top-right-radius: 0.5em;
+      border-bottom-left-radius: 0.5em;
+      border-bottom-right-radius: 0.5em;
+
+      > summary {
+        padding: 0.75em;
+        cursor: pointer;
+        font-size: 1.2em;
+        font-weight: 500;
+        background-color: color-mix(
+          in srgb,
+          var(--coreui-color-primary) 10%,
+          transparent
+        );
+
+        &:hover {
+          background-color: color-mix(
+            in srgb,
+            var(--coreui-color-primary) 20%,
+            transparent
+          );
+        }
+
+        &:active {
+          background-color: color-mix(
+            in srgb,
+            var(--coreui-color-primary) 15%,
+            transparent
+          );
+        }
+
+        transition: background-color 100ms ease-in;
+      }
+
+      &[open] > summary {
+        border-bottom: 1px solid;
+        border-color: color-mix(
+          in srgb,
+          var(--coreui-color-primary) 30%,
+          transparent
+        );
+      }
+
+      > div {
+        padding: 1em;
+      }
+    }
+  }
+
+  .Title {
+    fieldset {
+      padding: 0;
+      margin: 0;
+    }
+  }
+}
diff --git a/packages/libs/eda/src/lib/notebook/EdaNotebook.scss b/packages/libs/eda/src/lib/notebook/EdaNotebook.scss
deleted file mode 100644
index 0f0a50c61b..0000000000
--- a/packages/libs/eda/src/lib/notebook/EdaNotebook.scss
+++ /dev/null
@@ -1,35 +0,0 @@
-.EdaNotebook {
-  .Heading {
-    display: flex;
-    gap: 2em;
-    align-items: baseline;
-  }
-
-  .Paper {
-    max-width: 1250px;
-    padding: 1em;
-    margin: 1em auto;
-    background-color: #f3f3f3;
-    box-shadow: 0 0 2px #b5b5b5;
-
-    > * + * {
-      margin-block-start: 1rem;
-    }
-    h2,
-    h3 {
-      padding: 0;
-    }
-    h3 {
-      font-size: 1em;
-      font-weight: 400;
-      line-height: 1.5;
-    }
-  }
-
-  .Title {
-    fieldset {
-      padding: 0;
-      margin: 0;
-    }
-  }
-}
diff --git a/packages/libs/eda/src/lib/notebook/EdaNotebookAnalysis.tsx b/packages/libs/eda/src/lib/notebook/EdaNotebookAnalysis.tsx
index 327a3a7a20..6cb8f53d72 100644
--- a/packages/libs/eda/src/lib/notebook/EdaNotebookAnalysis.tsx
+++ b/packages/libs/eda/src/lib/notebook/EdaNotebookAnalysis.tsx
@@ -18,7 +18,7 @@ import { ExpandablePanel } from '@veupathdb/coreui';
 import { NotebookCell as NotebookCellType } from './Types';
 import { NotebookCell } from './NotebookCell';
 
-import './EdaNotebook.scss';
+import './EdaNotebook.css';
 
 interface NotebookSettings {
   /** Ordered array of notebook cells */
@@ -70,30 +70,26 @@ export function EdaNotebookAnalysis(props: Props) {
   );
   return (
     <div className="EdaNotebook">
-      <div className="Heading">
-        <h1>EDA Notebook</h1>
-      </div>
       <div className="Paper">
-        <div>
-          <h2>
+        <div className="Heading">
+          <h1>
             <SaveableTextEditor
               className="Title"
               value={analysisState.analysis?.displayName ?? ''}
               onSave={analysisState.setName}
             />
-          </h2>
-          <h3>Study: {safeHtml(studyRecord.displayName)}</h3>
+          </h1>
+          <h2>{safeHtml(studyRecord.displayName)}</h2>
         </div>
         {notebookSettings.cells.map((cell, index) => (
-          <ExpandablePanel title={cell.title} subTitle={{}} themeRole="primary">
-            <div style={{ padding: '1em' }}>
-              <NotebookCell
-                analysisState={analysisState}
-                cell={cell}
-                updateCell={(update) => updateCell(update, index)}
-              />
-            </div>
-          </ExpandablePanel>
+          <details>
+            <summary>{cell.title}</summary>
+            <NotebookCell
+              analysisState={analysisState}
+              cell={cell}
+              updateCell={(update) => updateCell(update, index)}
+            />
+          </details>
         ))}
       </div>
     </div>

From d3d696645d2e1fa96e78eabc5d4b760b374d29b3 Mon Sep 17 00:00:00 2001
From: Dave Falke <dfalke@uga.edu>
Date: Fri, 22 Nov 2024 13:10:15 -0500
Subject: [PATCH 4/4] Expose all coreui color definitions as css custom
 properties.

---
 .../coreui/src/components/theming/UIThemeProvider.tsx  | 10 ++++++++++
 1 file changed, 10 insertions(+)

diff --git a/packages/libs/coreui/src/components/theming/UIThemeProvider.tsx b/packages/libs/coreui/src/components/theming/UIThemeProvider.tsx
index b6c35d226c..db0c10088b 100644
--- a/packages/libs/coreui/src/components/theming/UIThemeProvider.tsx
+++ b/packages/libs/coreui/src/components/theming/UIThemeProvider.tsx
@@ -3,6 +3,7 @@ import { css, Global, ThemeProvider } from '@emotion/react';
 import { useCoreUIFonts } from '../../hooks';
 
 import { UITheme } from './types';
+import colors from '../../definitions/colors';
 
 export type UIThemeProviderProps = {
   theme: UITheme;
@@ -21,6 +22,15 @@ export default function UIThemeProvider({
       <Global
         styles={css`
           :root {
+            ${Object.entries(colors).flatMap(([colorName, byHueOrValue]) =>
+              typeof byHueOrValue === 'string'
+                ? [`--coreui-${colorName}: ${byHueOrValue};`]
+                : Object.entries(byHueOrValue).map(
+                    ([hueName, colorValue]) =>
+                      `--coreui-${colorName}-${hueName}: ${colorValue};`
+                  )
+            )}
+
             --coreui-color-primary: ${theme.palette.primary.hue[
               theme.palette.primary.level
             ]};