From 5aad4aa9fe9c5202ecf72fb05baaa26c71208efb Mon Sep 17 00:00:00 2001 From: Carl Vitullo Date: Thu, 6 Nov 2025 15:15:29 -0500 Subject: [PATCH 1/3] Add collapsible nested items to transcript table of contents MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implements individually collapsible list items for nested sections in transcript TOCs using native HTML details/summary elements. Each parent item with children can now be collapsed independently without JavaScript. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- src/components/Layout/MarkdownStyles.js | 27 ++++++++++++ src/helpers/rehypeWrapFirstList.ts | 57 +++++++++++++++++++++++++ src/helpers/retrieveMdPages.ts | 23 +++++++++- src/pages/transcripts/[slug].tsx | 2 +- 4 files changed, 106 insertions(+), 3 deletions(-) create mode 100644 src/helpers/rehypeWrapFirstList.ts diff --git a/src/components/Layout/MarkdownStyles.js b/src/components/Layout/MarkdownStyles.js index a667f893..71465dd6 100644 --- a/src/components/Layout/MarkdownStyles.js +++ b/src/components/Layout/MarkdownStyles.js @@ -34,5 +34,32 @@ export const MarkdownStyles = createGlobalStyle` word-break: break-word; word-wrap: break-word; } + li > details { + display: block; + } + summary { + cursor: pointer; + user-select: none; + list-style: none; + display: inline; + &::marker, + &::-webkit-details-marker { + display: none; + } + &::before { + content: '▶ '; + display: inline; + font-size: 0.75em; + transition: transform 0.15s ease; + display: inline-block; + width: 1em; + } + } + details[open] > summary::before { + transform: rotate(90deg); + } + details > ul { + margin-top: 0.5rem; + } } `; diff --git a/src/helpers/rehypeWrapFirstList.ts b/src/helpers/rehypeWrapFirstList.ts new file mode 100644 index 00000000..fcb5a532 --- /dev/null +++ b/src/helpers/rehypeWrapFirstList.ts @@ -0,0 +1,57 @@ +import { visit } from "unist-util-visit"; +import type { Root, Element, ElementContent } from "hast"; + +/** + * Rehype plugin that makes nested list items individually collapsible. + * For each
  • that contains a nested
      , wraps the content in + *
      to enable collapse/expand functionality. + */ +export default function rehypeWrapFirstList() { + return (tree: Root) => { + visit(tree, "element", (node) => { + // Only process
    • elements + if (node.tagName !== "li") { + return; + } + + // Check if this
    • has a nested
        child + const nestedUlIndex = node.children.findIndex( + (child): child is Element => + child.type === "element" && child.tagName === "ul", + ); + + // If no nested
          , nothing to do + if (nestedUlIndex === -1) { + return; + } + + // Split children into summary content (before ul) and nested ul + const summaryContent = node.children.slice(0, nestedUlIndex); + const nestedUl = node.children[nestedUlIndex] as Element; + + // Only wrap if there's content to put in the summary + if (summaryContent.length === 0) { + return; + } + + // Create the summary element with the content before the nested ul + const summary: Element = { + type: "element", + tagName: "summary", + properties: {}, + children: summaryContent as ElementContent[], + }; + + // Create the details element + const details: Element = { + type: "element", + tagName: "details", + properties: { open: true }, // Open by default + children: [summary, nestedUl], + }; + + // Replace the
        • 's children with just the details element + node.children = [details]; + }); + }; +} diff --git a/src/helpers/retrieveMdPages.ts b/src/helpers/retrieveMdPages.ts index 4a45dec3..6cf905fe 100644 --- a/src/helpers/retrieveMdPages.ts +++ b/src/helpers/retrieveMdPages.ts @@ -10,6 +10,7 @@ import rehypeStringify from "rehype-stringify"; import rehypeSlug from "rehype-slug"; import remarkHeadings, { hasHeadingsData } from "@vcarl/remark-headings"; import { toString } from "mdast-util-to-string"; +import rehypeWrapFirstList from "./rehypeWrapFirstList"; const loadMd = async (path: string) => { const fullPath = join(process.cwd(), `${path}.md`); @@ -39,8 +40,26 @@ const remarkHtmlProcessor = unified() .use(rehypeSlug) .use(rehypeStringify, { allowDangerousHtml: true }); -export const processMd = (mdSource: string) => { - const vfile = remarkHtmlProcessor.processSync(mdSource); +export interface ProcessMdOptions { + wrapFirstList?: boolean; +} + +export const processMd = (mdSource: string, options?: ProcessMdOptions) => { + let processor = remarkHtmlProcessor; + + // If wrapFirstList is enabled, add the rehype plugin + if (options?.wrapFirstList) { + processor = unified() + .use(parse) + .use(remarkGfm) + .use(remarkHeadings as ReturnType["use"]>) + .use(remarkRehype, { allowDangerousHtml: true }) + .use(rehypeSlug) + .use(rehypeWrapFirstList) + .use(rehypeStringify, { allowDangerousHtml: true }); + } + + const vfile = processor.processSync(mdSource); if (hasHeadingsData(vfile.data)) { return { html: vfile.toString(), headings: vfile.data.headings }; } diff --git a/src/pages/transcripts/[slug].tsx b/src/pages/transcripts/[slug].tsx index e9d4dbaf..8cb665e2 100644 --- a/src/pages/transcripts/[slug].tsx +++ b/src/pages/transcripts/[slug].tsx @@ -88,7 +88,7 @@ export const getStaticProps = async ({ props: { all, ...pick(["title", "date"], doc), - html: processMd(doc.content).html, + html: processMd(doc.content, { wrapFirstList: true }).html, description: processMdPlaintext(doc.description).html, }, }; From 3c3ee6524bc1adcf055bcbff4237c012ac6f6bd4 Mon Sep 17 00:00:00 2001 From: Carl Vitullo Date: Sat, 8 Nov 2025 14:14:10 -0500 Subject: [PATCH 2/3] Change summary element to display: block MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Updates TOC summary elements to use block display instead of inline for better layout control while keeping the arrow inline. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- src/components/Layout/MarkdownStyles.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Layout/MarkdownStyles.js b/src/components/Layout/MarkdownStyles.js index 71465dd6..316b2aec 100644 --- a/src/components/Layout/MarkdownStyles.js +++ b/src/components/Layout/MarkdownStyles.js @@ -41,7 +41,7 @@ export const MarkdownStyles = createGlobalStyle` cursor: pointer; user-select: none; list-style: none; - display: inline; + display: block; &::marker, &::-webkit-details-marker { display: none; From 98db3667a287031f0580b969fcf4a47109becf71 Mon Sep 17 00:00:00 2001 From: Carl Vitullo Date: Sun, 9 Nov 2025 14:17:04 -0500 Subject: [PATCH 3/3] Improve rendering of table of contents for TMiR --- src/components/Layout/LayoutStyles.js | 6 ------ src/components/Layout/MainStyles.js | 11 ++++++----- src/components/Layout/MarkdownStyles.js | 6 +++--- 3 files changed, 9 insertions(+), 14 deletions(-) diff --git a/src/components/Layout/LayoutStyles.js b/src/components/Layout/LayoutStyles.js index 597caf24..32be2483 100644 --- a/src/components/Layout/LayoutStyles.js +++ b/src/components/Layout/LayoutStyles.js @@ -57,15 +57,9 @@ const standardLayout = css` } details { - margin-bottom: 1rem; - & summary { cursor: pointer; } - - &:last-of-type { - margin-bottom: 2rem; - } } blockquote { diff --git a/src/components/Layout/MainStyles.js b/src/components/Layout/MainStyles.js index 384c6dbc..b27db7fa 100644 --- a/src/components/Layout/MainStyles.js +++ b/src/components/Layout/MainStyles.js @@ -62,14 +62,11 @@ html body { } ul, ol { - padding-left: 3rem; - + padding-left: 1.5rem; + padding-top: 0.5rem; li { margin-bottom: 0.5rem; } - li:first-of-type { - margin-top: 0.5rem; - } } h1, h2, h3, h4, h5, h6 { @@ -136,5 +133,9 @@ html body { text-decoration: underline; } } + + summary::before { + margin-right: 0.5rem; + } } `; diff --git a/src/components/Layout/MarkdownStyles.js b/src/components/Layout/MarkdownStyles.js index 316b2aec..1a833471 100644 --- a/src/components/Layout/MarkdownStyles.js +++ b/src/components/Layout/MarkdownStyles.js @@ -29,6 +29,9 @@ export const MarkdownStyles = createGlobalStyle` text-decoration: underline; } } + code { + font-size: 80%; + } a, code, strong { white-space: pre-wrap; word-break: break-word; @@ -58,8 +61,5 @@ export const MarkdownStyles = createGlobalStyle` details[open] > summary::before { transform: rotate(90deg); } - details > ul { - margin-top: 0.5rem; - } } `;