actualNoteContentPart: # Functional React component: AmplenoteSidebarToc
  Here is the functional React component that GitClear is using to load a table of contents alongside its Amplenote page embed:
  
  First, amplenote-sidebar-toc.js. Note that lodash is not strictly necessary, but scroll events fire a lot, so your page could experience some amount of slowdown if you don't use some flavor of "debounce" or "throttle" for handling these noisy events: 
  ```
  import { throttle } from "lodash"import { useCallback, useEffect, useRef, useState } from "react" import "./amplenote-sidebar-toc.scss" const MAX_ITEMS_TO_SEARCH_FOR_TOC = 3;const MIN_SECTIONS_TO_SHOW = 5;const THROTTLE_MS = 500;const TOP_ROW_HEIGHT = 40; // --------------------------------------------------------------------------function ensureSelectedLinkVisible(highlightedAnchorName, stickyParentRef) {  if (stickyParentRef.current && highlightedAnchorName) {    const listContainerEl = stickyParentRef.current;    // Can't use querySelector directly since name attributes can contain punctuation, which causes "invalid querySelector" exception    const headerListAnchorEl = Array.from(listContainerEl.querySelectorAll("a")).find(a => a.getAttribute("name") === `sidebar-${ highlightedAnchorName }`);    if (headerListAnchorEl && (headerListAnchorEl.offsetTop > (listContainerEl.offsetHeight + listContainerEl.scrollTop) ||        (headerListAnchorEl.offsetTop < listContainerEl.scrollTop))) {      listContainerEl.scrollTo({ top: headerListAnchorEl.offsetTop - listContainerEl.offsetHeight / 2, behavior: "smooth" });    }  }} // --------------------------------------------------------------------------function receiveAmplenoteToc(element, setContentSections, toc) {  if (toc && toc.length >= MIN_SECTIONS_TO_SHOW) {    setContentSections(toc);  } else {    console.debug("Not showing Amplenote TOC - only", toc ? toc.length : 0, "sections received");  }} // --------------------------------------------------------------------------function renderContentSections(contentSections, highlightedSectionAnchor, onHeaderClick) {  let depthsTraversed = {};  const traverseContentSections = sectionsExcludingToc(contentSections);   return (    <div className="header_list_container">      {        traverseContentSections.map(headerDetail => {          let result = (            <a              className={ `header_link ${ headerDetail.tagName }` }              href={ headerDetail.href }              name={ `sidebar-${ headerDetail.anchorName }` }              onClick={ onHeaderClick }            >              { headerDetail.text }            </a>          );           // Sometimes the content author decides to skip a header level (e.g., going from h1 to h3) but we don't          // want to double-indent if there was no heading above this at the intermediary levels, so we subtract any levels that          // weren't visited in the traverse toward this header depth          if (Object.keys(depthsTraversed).length > headerDetail.depth) {            Object.keys(depthsTraversed).forEach(key => key > headerDetail.depth ? (delete depthsTraversed[key]) : null);          }          depthsTraversed[headerDetail.depth] = true;          const depthsSkipped = headerDetail.depth - Object.keys(depthsTraversed).length;           for (let i = (headerDetail.depth - depthsSkipped); i > 1; i--) {            result = (<div className={ `header_depth_${ i }` }>{ result }</div>);          }           // Wrap the anchor in a div that can be targeted. This div might wrap the 0-many nested divs (creating levels of depth for the link)          return (            <div              className={ `header_row ${ highlightedSectionAnchor === headerDetail.anchorName ? "current_scrolled_section" : "" }` }              key={ `header-${ headerDetail.index }` }            >              { result }            </div>          );        })      }    </div>  )} // --------------------------------------------------------------------------function sectionsExcludingToc(contentSections) {  let foundToc = false;  return contentSections.filter((section, index) => {    if (index >= MAX_ITEMS_TO_SEARCH_FOR_TOC && !foundToc) return true;     if (section.text?.toLowerCase() === "table of contents") {      foundToc = section.depth;      return false;    } else if (foundToc) {      if (section.depth <= foundToc) {        foundToc = false;        return true;      } else {        return false;      }    } else {      return true;    }  })} // --------------------------------------------------------------------------function findStickyParent(sidebarRef, stickyParentRef) {  if (sidebarRef.current) {    let element = sidebarRef.current.parentElement;    while (element) {      const computedStyle = window.getComputedStyle(element);      if (computedStyle.position === "sticky") {        stickyParentRef.current = element;        break;      }      element = element.parentElement;    }  }} // --------------------------------------------------------------------------function setupAmplenoteTocEventHandlers(handleTocUpdate, highlightHeaderRef, handleScrollHighlight, scrollToAnchorRef, setHighlightedSection) {  window.onAmpleEmbedTOC = handleTocUpdate;  window.onAmpleEmbedHeadingHighlight = function(headerDetail) {    handleScrollHighlight(headerDetail, setHighlightedSection);  };   return () => {    window.onAmpleEmbedTOC = null;    window.onAmpleEmbedHeadingHighlight = null;    highlightHeaderRef.current = null;    scrollToAnchorRef.current = null;  }} // --------------------------------------------------------------------------function useScrollListener(queryHeaderRef, ) {  useEffect(() => {    const onContentScroll = () => {      if (queryHeaderRef.current) {        queryHeaderRef.current();      }    }     const throttled = throttle(onContentScroll, THROTTLE_MS);    window.addEventListener("scroll", throttled, { passive: true });     return () => {      window.removeEventListener("scroll", throttled);    };  }, []);} // -----------------------------------------------------------------------------export function useWindowHeight(viewerRef, heightOffset = 0, debounceMs = THROTTLE_MS) {  const setViewRefHeight = () => {    if (viewerRef.current) {      viewerRef.current.style.maxHeight = `${ window.innerHeight - heightOffset }px`;    }  }  useEffect(() => {    const resizeListener = throttle(() => {      setViewRefHeight();    }, THROTTLE_MS);    setViewRefHeight();    window.addEventListener("resize", resizeListener);    return () => {      window.removeEventListener("resize", resizeListener);    }  }, [ viewerRef ]);} // --------------------------------------------------------------------------// // React component to manage highlighting sidebar links based on scroll position (for Amplenote content)export default function AmplenoteSidebarToc({ title }) {  const [ collapsed, setCollapsed ] = useState(false);  const [ contentSections, setContentSections ] = useState([]);  const [ highlightedSectionAnchor, setHighlightedSectionAnchor ] = useState(null);   const highlightHeaderRef = useRef(null);  const queryHeaderRef = useRef(null);  const sidebarRef = useRef(null);  const scrollToAnchorRef = useRef(null);  const stickyParentRef = useRef(null);   const handleTocUpdate = useCallback((element, toc, scrollToAnchorName, queryHighlightedHeader) => {    if (scrollToAnchorRef) scrollToAnchorRef.current = scrollToAnchorName;    if (queryHighlightedHeader) queryHeaderRef.current = queryHighlightedHeader;    receiveAmplenoteToc(element, setContentSections, toc)  }, [ setContentSections ]);   const onHeaderClick = useCallback(event => {    event.preventDefault();     const anchorName = event.currentTarget.getAttribute("name").replace("sidebar-", "");    history.replaceState(null, "", "#" + anchorName);    if (scrollToAnchorRef.current) scrollToAnchorRef.current(anchorName);    if (highlightHeaderRef.current) highlightHeaderRef.current();  }, [ contentSections ]);   const handleScrollHighlight = useCallback((headerDetail, setHighlightedSectionAnchor) => {    if (headerDetail?.anchorName) {      setHighlightedSectionAnchor(headerDetail.anchorName);    }  }, []);   useEffect(() => ensureSelectedLinkVisible(highlightedSectionAnchor, stickyParentRef),    [ highlightedSectionAnchor ]);   useEffect(() => {    return setupAmplenoteTocEventHandlers(handleTocUpdate, highlightHeaderRef, handleScrollHighlight,      scrollToAnchorRef, setHighlightedSectionAnchor);  }, []);   useEffect(() => {    findStickyParent(sidebarRef, stickyParentRef);  }, [ contentSections ]);   useScrollListener(queryHeaderRef);  useWindowHeight(stickyParentRef, TOP_ROW_HEIGHT);   return (    <div      className={ `amplenote_sidebar_toc_container ${ contentSections.length ? "with_content" : "" }${ collapsed ? " is_collapsed" : "" }` }      ref={ sidebarRef }    >      <div className="sidebar_header">        <h3 className="sidebar_label" title={ title ? `"${ title }" section index` : null }>{ title || "Page Contents" }</h3>        <div className="sidebar_collapser" onClick={ () => setCollapsed(!collapsed) }>          <i className={ `fas fa-caret-${ collapsed ? "down" : "right" }` } />        </div>      </div>      {        renderContentSections(contentSections, highlightedSectionAnchor, onHeaderClick)      }    </div>  )}
  ```
  
  Then, amplenote-sidebar-toc.scss. Note that this code presumes that your initial React component is mounted in a div with data-react-class="AmplenoteSidebarToc", which must be a direct child of the parent div that contains your Amplenote embedded content. More on that in the next section.  
  ```
  // scssdiv[data-react-class="AmplenoteSidebarToc"] {  &:has(.amplenote_sidebar_toc_container.with_content) {    background-color: $color-background-primary;    border: 1px solid $color-border-primary;    border-radius: 5px;    margin-left: 20px;    margin-bottom: 10px;    max-height: calc(100vh - 20px);    overflow-y: auto;    position: sticky;    top: 10px;  }   &:has(.amplenote_sidebar_toc_container.with_content.is_collapsed) {    background-color: transparent;    border: none;    overflow: hidden;  }   .amplenote_sidebar_toc_container {    display: none;    overflow: hidden;    transition: width 0.3s ease;    width: 0;     &.with_content {      @include breakpoint(tablet) {        left: 0;        display: block;        margin-left: 5px;        width: 300px;        padding: 10px 0;        top: 10px;      }       @include breakpoint(laptop) {        width: 350px;      }       @include breakpoint(desktop) {        width: 400px;      }       &.is_collapsed {        width: 60px;         .header_list_container,        .sidebar_label {          display: none;        }      }    }     .sidebar_header {      display: flex;      justify-content: space-between;      padding-left: 10px;      padding-right: 10px;       .sidebar_label {        font-weight: 400;        margin: 0;        overflow: hidden;        padding: 4px 0;        text-transform: uppercase;        white-space: nowrap;        text-overflow: ellipsis;      }       .sidebar_collapser {        align-items: center;        border: 1px solid $color-border-primary;        border-radius: 5px;        cursor: pointer;        display: flex;        justify-content: center;        margin-right: 6px;        padding: 5px 10px;         &:hover {          background-color: $color-background-light-glow;        }      }    }     .header_link {      border-radius: 5px;      display: block;      padding: 5px 15px 5px 10px;      transition: 400ms background-color ease;       &:hover {        background-color: $color-background-blueish;      }       &:active {        background-color: $color-background-blueish-secondary;      }       &.h1 {        margin: 4px 0 2px 0;      }       &.h2, &.h3, &.h4 {        border-bottom-left-radius: 0;        border-top-left-radius: 0;      }    }     .header_depth_2,    .header_depth_3,    .header_depth_4 {      border-left: 1px solid $color-border-secondary;      margin-left: 12px;    }  }   .current_scrolled_section {    font-weight: bold;  }}
  ```
processedNoteContent: ---
  title: Amplenote: Embeddable Sidebar Table of Contents...
  headers: # Functional React component: AmplenoteSidebarToc
  ---
  Functional React component: AmplenoteSidebarToc
  Here is the functional React component that GitClear is using to load a table of contents alongside its Amplenote page embed: First, amplenote-sidebar-toc.js. Note that lodash is not strictly necessary, but scroll events fire a lot, so your page could experience some amount of slowdown if you don't use some flavor of "debounce" or "throttle" for handling these noisy events: Then, amplenote-sidebar-toc.scss. Note that this code presumes that your initial React component is mounted in a div with data-react-class="AmplenoteSidebarToc", which must be a direct child of the parent div that contains your Amplenote embedded content. More on that in the next section. // scssdiv[data-react-class="AmplenoteSidebarToc"] {  &:has(.amplenote_sidebar_toc_container.with_content) {    background-color: $color-background-primary;    border: 1px solid $color-border-primary;    border-radius: 5px;    margin-left: 20px;    margin-bottom: 10px;    max-height: calc(100vh - 20px);    overflow-y: auto;    position: sticky;    top: 10px;  }   &:has(.amplenote_sidebar_toc_container.with_content.is_collapsed) {    background-color: transparent;    border: none;    overflow: hidden;  }   .amplenote_sidebar_toc_container {    display: none;    overflow: hidden;    transition: width 0.3s ease;    width: 0;     &.with_content {      @include breakpoint(tablet) {        left: 0;        display: block;        margin-left: 5px;        width: 300px;        padding: 10px 0;        top: 10px;      }       @include breakpoint(laptop) {        width: 350px;      }       @include breakpoint(desktop) {        width: 400px;      }       &.is_collapsed {        width: 60px;         .header_list_container,        .sidebar_label {          display: none;        }      }    }     .sidebar_header {      display: flex;      justify-content: space-between;      padding-left: 10px;      padding-right: 10px;       .sidebar_label {        font-weight: 400;        margin: 0;        overflow: hidden;        padding: 4px 0;        text-transform: uppercase;        white-space: nowrap;        text-overflow: ellipsis;      }       .sidebar_collapser {        align-items: center;        border: 1px solid $color-border-primary;        border-radius: 5px;        cursor: pointer;        display: flex;        justify-content: center;        margin-right: 6px;        padding: 5px 10px;         &:hover {          background-color: $color-background-light-glow;        }      }    }     .header_link {      border-radius: 5px;      display: block;      padding: 5px 15px 5px 10px;      transition: 400ms background-color ease;       &:hover {        background-color: $color-background-blueish;      }       &:active {        background-color: $color-background-blueish-secondary;      }       &.h1 {        margin: 4px 0 2px 0;      }       &.h2, &.h3, &.h4 {        border-bottom-left-radius: 0;        border-top-left-radius: 0;      }    }     .header_depth_2,    .header_depth_3,    .header_depth_4 {      border-left: 1px solid $color-border-secondary;      margin-left: 12px;    }  }   .current_scrolled_section {    font-weight: bold;  }}
noteTitle: Amplenote: Embeddable Sidebar Table of Contents React Component HTML, Javascript & CSS