You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
This is a backlog item. It asks for a clearer way to show heading depth in the TOC. No code starts until the maintainer picks one direction from a visual preview.
The two screenshots from Edward Saperia are not in this issue. Please drag them in.
No open issue or discussion asks for this alignment. The search date is 2026-09-29.
What happened
Edward Saperia wrote in chat on 2026-09-26: "Still confusing that these are the same level:"
Screenshot 1 is Wide TOC. Voting sits at the root. House London #1 is the chat-open row. It has a fold button, and the title starts with a pin. House London #0 is the scroll-spy row. It has no fold button. Its pin sits in the fold-button column of the row above. People has an open fold button. The two House London rows are one heading level.
Screenshot 2: Schedule sits at the root. Tables, Teams, How to Submit, and Submissions are one level under it. Tables and Teams have a fold button. How to Submit has no fold button and no emoji. Submissions is expanded. Virtual Power Plant is one level under Submissions, and its title starts with a lock emoji.
The fold button paints a chevron. A parent row has that button. A leaf row does not. The title then starts further left. A reader cannot see that the rows share a heading level.
What we expect
Rows of one heading level share one horizontal start for the icon and the title. A parent row and a leaf row still read as one level. Folded, expanded, chat-open, scroll-spy, and hover states keep that geometry. Wide TOC and TocModal both do this. The Tick rail stays as it is.
Steps to reproduce
Open a pad on desktop with Wide TOC showing.
Add two headings of one level. Give only the first heading a child heading.
Start both titles with the same emoji.
Compare the emoji start.
Open chat on the parent row. Scroll until the leaf row is the scroll-spy row.
Open the same pad in TocModal and compare those rows.
Findings
These desktop numbers were measured in the browser on 2026-09-29. The page was the /editor playground, in the light theme, with a root font of 16px. TocModal was not opened. Dark theme and the premium themes were not measured.
Row layout.TocRow is three grid children. They are the leading slot, the title link, and TocRowTrail. The row measured as a grid, with a column gap of 8px and padding-inline of 12px. daisyUI 5.7.32 sets the menu row columns to minmax(auto, max-content) auto max-content. That value is read from menu.css in daisyUI 5.7.32.
The nested list ul.toc__children sets the indent for each level. It measured margin-inline-start of 16px and padding of 0. Row boxes sat at x 18, then x 34, then x 50. Each step is 16px.
Fold button.TocItemBody renders the fold button only when nestedNodes.length > 0. On mobile, a leaf passes leading as null. On desktop, a leaf omits the fold button. The leading slot element stays in the grid. An empty leading slot collapses, and the title shifts left. The leading slot measured 24px when the fold button was present. It measured 0px on a leaf.
The desktop fold button uses size-5. Its width measured 20px. The other 4px is gap-1 between that button and the drag wrapper. The drag handle is absolutely positioned at left: -22px. It adds no width in the row flow. The level badge stays hidden until a drag.
House London #1 was the parent row. Its title link measured x = 78. House London #0 was the leaf row. Its title link measured x = 54. The leaf title starts 24px further left.
Heading emoji.useToc copies node.textContent into the title. The pin and the lock are characters in that string. This is read from useToc and TocItemBody. The measured titles included the pin.
State color. Chat-open adds menu-active. Scroll-spy adds menu-focus when that row is not chat-open. Hover uses --toc-row-hover-bg. In the light theme, menu-active measured background oklch(0.903582 0.0336126 257.863) and text rgb(26, 115, 232). menu-focus measured background rgb(220, 227, 237) and text rgb(15, 23, 42). The hover token was color-mix(in oklch, #0f172a 14%, #dce3ed). These classes change color. They do not insert the fold button. That split is read from TocRow.
Rail. The line is ul.toc__children::before. It measured an inline start of 10px, a width of 1px, and opacity 0.1. --border is 1px. The list padding measured 0px 6px 0px 10px. The list class includes Tailwind p-0. The SCSS rule .toc__list still sets that padding. daisyUI's nested list uses margin-inline-start: 1rem and padding-inline-start: 0.5rem. The TOC rule sets that padding to 0. It also moves the line to 10px.
Semantics. The list is a ul of li elements with class menu. A live row had no role, no aria-level, and no aria-expanded. The fold button label was "Collapse section". No key handler lives under components/toc. A button still accepts Enter and Space. Tree arrow keys are read from code, not measured.
Three surfaces. The numbers above are Wide TOC. The Tick rail is a 32px dash list. It has no fold button and no title row. That description is read from TocTickRail and CONTEXT.md §Pad outline. The playground did not mount the Tick rail. TocModal uses the same TocRow and the same toc__children list. Its fold button uses size-11. A size-11 button on the playground measured 44 by 44 px. The phone drawer was not opened. The 44px title shift is read from code, not measured. Mobile shows the chat control at all times. The grip is desktop only.
Next step: pick one direction below. Do not start code in this task.
Reference points
These sources were checked on 2026-09-29. Unopened claims are left out. That set is Material 3, Notion, Google Docs, Confluence, and Obsidian.
WAI-ARIA tree view: https://www.w3.org/WAI/ARIA/apg/patterns/treeview/
A parent node sets aria-expanded to true or false. An end node omits aria-expanded. With that attribute, a screen reader treats the node as a parent. The pattern uses roles tree, treeitem, and group. Right arrow opens a closed node. Left arrow closes an open node. The guide does not require a reserved visual gutter.
VS Code tree renderer: https://github.com/microsoft/vscode/blob/main/src/vs/base/browser/ui/tree/abstractTree.ts
The renderer always appends the twistie element. A node that cannot collapse keeps the element and removes the collapsible class. TreeRenderer.DefaultIndent is 8. Indent guides use .indent-guide and a 1px border. The CSS sets guide opacity to 0. The renderer can raise it on hover, or when guides are set to always.
Fluent 2 tree: https://fluent2.microsoft.design/components/web/react/core/tree/usage
The page says: "Indentation from the parent item is 24 pixels for medium size items, and 12 pixels for small. If the child item is a leaf, an additional 24 pixels is added (regardless of size) to account for the space that the chevron would otherwise occupy."
These decisions stay. An idea that breaks one must name it and bring new evidence.
TOC channel-map rework. Do not bring back the data-level type ladder, the chat-open accent bar, or the scroll-spy wash.
Wide TOC rails. Use the daisyUI nested line on toc__children only. Keep the stock opacity. Do not add L-elbows. Do not strengthen ::before.
Titles wrap. Do not truncate them.
Presence stays in TocRowTrail, inside the column. Do not hang it past the column edge.
Tick rail. It is session-only and 32px wide. Do not persist 32 or 240. Do not export TocTickRail from toc/index.ts. Do not add SideContinuum. Wide TOC scroll-spy stays menu-focus. Tick rail spy stays bg-primary. A short tick stack sits in the middle of the live rail.
Candidate directions
Do not pick one here.
Reserved fold gutter. Every row keeps the fold-button width, with or without children. The icon and the title then share one x per level. Trade-off: a leaf row keeps an empty box of 24px on desktop and 44px on mobile. This direction does not touch a settled rule.
Same-size leading mark. A parent row shows the chevron. A leaf row shows a blank or a dot in a box of the same size. Trade-off: a dot can compete with the emoji in the title. Do not turn the mark into the data-level type ladder.
Depth mark in a fixed column. The title always starts after that column. Rows of one heading level share the column. Trade-off: a narrow Wide TOC gains one more column. This mark is not the Tick rail. A color per level needs new evidence against the channel-map ruling.
Color that never moves the row. Chat-open, scroll-spy, and hover change color only. Depth still comes from the gutter. Trade-off: color alone does not show depth. Do not replace menu-focus with a brand wash. Do not add a chat-open accent bar.
Fixed chevron box. Paint the chevron in a box that does not change the title column. Trade-off: the icon can cover the first emoji. The grip already sits at left: -22px. Do not move presence.
Darker indent guides. Trade-off: this overturns the stock rail opacity rule. It needs new evidence. Do not add L-elbows.
Acceptance criteria
The maintainer approves one direction from a visual preview before any code. The preview is a Cursor canvas or a standalone HTML demo, with one mock per variant.
Same-level rows share one x for the icon and the title, whether or not they have children.
Parent, leaf, folded, expanded, chat-open, scroll-spy, and hover states keep the row geometry.
Keyboard and screen reader semantics are stated and tested.
Wide TOC and TocModal are both covered.
Light, dark, and the premium themes are checked.
Existing TOC Cypress specs pass, and one new spec pins the alignment.
Blocked by
None.
Agent brief
Type: HITL
Category: enhancement
Current behavior: A parent row renders a fold button. A leaf row collapses the leading slot. The title starts further left. On the measured Wide TOC, that shift was 24px. Chat-open and scroll-spy change color only. The list is a ul of li elements with class menu. It is not an ARIA tree.
Desired behavior: The maintainer picks one direction from the preview. Rows of one heading level then share one start for the icon and the title. State changes do not move that start. Wide TOC and TocModal match. The settled list above still holds.
Where to start:apps/webapp/src/components/toc/TocRow.tsx (TocRow). apps/webapp/src/components/toc/TocItemBody.tsx (leading, hasChildren). apps/webapp/src/components/toc/TocRowTrail.tsx. apps/webapp/src/styles/components/_tableOfContents.scss. apps/webapp/src/styles/components/_tocDrag.scss (.toc-drag-handle). apps/webapp/src/components/pages/document/components/TocModal.tsx. Leave TocTickRail alone unless a chosen direction says otherwise. Search by symbol. File names are hints as of 2026-09-29.
Rules that apply: Root CLAUDE.md holds the Settled list. apps/webapp/CLAUDE.md §TOC And Heading Actions and §Pad Workspace Surfaces apply. CONTEXT.md §Pad outline names Wide TOC and the Tick rail. .cursor/docs/design-system.md §Table of contents and §State language apply. AGENTS.md requires the visual preview before UI code. Keep the Toc* names. Do not rebrand the folder to Outline.
Verify: Build the preview first and wait for one approved direction. Then check Wide TOC and TocModal in light, dark, and the premium themes. Parent and leaf rows of one level share one x. Fold, expand, chat-open, scroll-spy, and hover do not move that x. State the keyboard and screen reader behavior and test it. Run the existing TOC Cypress specs. Add one spec that pins the alignment.
This is a backlog item. It asks for a clearer way to show heading depth in the TOC. No code starts until the maintainer picks one direction from a visual preview.
The two screenshots from Edward Saperia are not in this issue. Please drag them in.
Related
What happened
Edward Saperia wrote in chat on 2026-09-26: "Still confusing that these are the same level:"
Screenshot 1 is Wide TOC. Voting sits at the root. House London #1 is the chat-open row. It has a fold button, and the title starts with a pin. House London #0 is the scroll-spy row. It has no fold button. Its pin sits in the fold-button column of the row above. People has an open fold button. The two House London rows are one heading level.
Screenshot 2: Schedule sits at the root. Tables, Teams, How to Submit, and Submissions are one level under it. Tables and Teams have a fold button. How to Submit has no fold button and no emoji. Submissions is expanded. Virtual Power Plant is one level under Submissions, and its title starts with a lock emoji.
The fold button paints a chevron. A parent row has that button. A leaf row does not. The title then starts further left. A reader cannot see that the rows share a heading level.
What we expect
Rows of one heading level share one horizontal start for the icon and the title. A parent row and a leaf row still read as one level. Folded, expanded, chat-open, scroll-spy, and hover states keep that geometry. Wide TOC and TocModal both do this. The Tick rail stays as it is.
Steps to reproduce
Findings
These desktop numbers were measured in the browser on 2026-09-29. The page was the
/editorplayground, in the light theme, with a root font of 16px. TocModal was not opened. Dark theme and the premium themes were not measured.Row layout.
TocRowis three grid children. They are the leading slot, the title link, andTocRowTrail. The row measured as a grid, with a column gap of 8px and padding-inline of 12px. daisyUI 5.7.32 sets the menu row columns tominmax(auto, max-content) auto max-content. That value is read frommenu.cssin daisyUI 5.7.32.The nested list
ul.toc__childrensets the indent for each level. It measuredmargin-inline-startof 16px and padding of 0. Row boxes sat at x 18, then x 34, then x 50. Each step is 16px.Fold button.
TocItemBodyrenders the fold button only whennestedNodes.length > 0. On mobile, a leaf passesleadingas null. On desktop, a leaf omits the fold button. The leading slot element stays in the grid. An empty leading slot collapses, and the title shifts left. The leading slot measured 24px when the fold button was present. It measured 0px on a leaf.The desktop fold button uses
size-5. Its width measured 20px. The other 4px isgap-1between that button and the drag wrapper. The drag handle is absolutely positioned atleft: -22px. It adds no width in the row flow. The level badge stays hidden until a drag.House London #1 was the parent row. Its title link measured x = 78. House London #0 was the leaf row. Its title link measured x = 54. The leaf title starts 24px further left.
Heading emoji.
useToccopiesnode.textContentinto the title. The pin and the lock are characters in that string. This is read fromuseTocandTocItemBody. The measured titles included the pin.State color. Chat-open adds
menu-active. Scroll-spy addsmenu-focuswhen that row is not chat-open. Hover uses--toc-row-hover-bg. In the light theme,menu-activemeasured backgroundoklch(0.903582 0.0336126 257.863)and textrgb(26, 115, 232).menu-focusmeasured backgroundrgb(220, 227, 237)and textrgb(15, 23, 42). The hover token wascolor-mix(in oklch, #0f172a 14%, #dce3ed). These classes change color. They do not insert the fold button. That split is read fromTocRow.Rail. The line is
ul.toc__children::before. It measured an inline start of 10px, a width of 1px, and opacity 0.1.--borderis 1px. The list padding measured0px 6px 0px 10px. The list class includes Tailwindp-0. The SCSS rule.toc__liststill sets that padding. daisyUI's nested list usesmargin-inline-start: 1remandpadding-inline-start: 0.5rem. The TOC rule sets that padding to 0. It also moves the line to 10px.Semantics. The list is a
uloflielements with classmenu. A live row had norole, noaria-level, and noaria-expanded. The fold button label was "Collapse section". No key handler lives undercomponents/toc. A button still accepts Enter and Space. Tree arrow keys are read from code, not measured.Three surfaces. The numbers above are Wide TOC. The Tick rail is a 32px dash list. It has no fold button and no title row. That description is read from
TocTickRailandCONTEXT.md§Pad outline. The playground did not mount the Tick rail. TocModal uses the sameTocRowand the sametoc__childrenlist. Its fold button usessize-11. Asize-11button on the playground measured 44 by 44 px. The phone drawer was not opened. The 44px title shift is read from code, not measured. Mobile shows the chat control at all times. The grip is desktop only.Next step: pick one direction below. Do not start code in this task.
Reference points
These sources were checked on 2026-09-29. Unopened claims are left out. That set is Material 3, Notion, Google Docs, Confluence, and Obsidian.
WAI-ARIA tree view: https://www.w3.org/WAI/ARIA/apg/patterns/treeview/
A parent node sets
aria-expandedto true or false. An end node omitsaria-expanded. With that attribute, a screen reader treats the node as a parent. The pattern uses rolestree,treeitem, andgroup. Right arrow opens a closed node. Left arrow closes an open node. The guide does not require a reserved visual gutter.VS Code tree CSS: https://github.com/microsoft/vscode/blob/main/src/vs/base/browser/ui/tree/media/tree.css
.monaco-tl-twistieis 16px wide, withflex-shrink: 0anddisplay: flex.VS Code tree renderer: https://github.com/microsoft/vscode/blob/main/src/vs/base/browser/ui/tree/abstractTree.ts
The renderer always appends the twistie element. A node that cannot collapse keeps the element and removes the
collapsibleclass.TreeRenderer.DefaultIndentis 8. Indent guides use.indent-guideand a 1px border. The CSS sets guide opacity to 0. The renderer can raise it on hover, or when guides are set to always.Fluent 2 tree: https://fluent2.microsoft.design/components/web/react/core/tree/usage
The page says: "Indentation from the parent item is 24 pixels for medium size items, and 12 pixels for small. If the child item is a leaf, an additional 24 pixels is added (regardless of size) to account for the space that the chevron would otherwise occupy."
Apple outline view, archive: https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/OutlineView/Articles/AboutOutlineViews.html
An expandable item shows a disclosure triangle. Items inside an expanded item are indented. The page does not say a leaf reserves the triangle width. The current HIG page did not return body text. It is not cited.
Do not re-propose
These decisions stay. An idea that breaks one must name it and bring new evidence.
toc__childrenonly. Keep the stock opacity. Do not add L-elbows. Do not strengthen::before.TocRowTrail, inside the column. Do not hang it past the column edge.TocTickRailfromtoc/index.ts. Do not addSideContinuum. Wide TOC scroll-spy staysmenu-focus. Tick rail spy staysbg-primary. A short tick stack sits in the middle of the live rail.Candidate directions
Do not pick one here.
Reserved fold gutter. Every row keeps the fold-button width, with or without children. The icon and the title then share one x per level. Trade-off: a leaf row keeps an empty box of 24px on desktop and 44px on mobile. This direction does not touch a settled rule.
Same-size leading mark. A parent row shows the chevron. A leaf row shows a blank or a dot in a box of the same size. Trade-off: a dot can compete with the emoji in the title. Do not turn the mark into the data-level type ladder.
Depth mark in a fixed column. The title always starts after that column. Rows of one heading level share the column. Trade-off: a narrow Wide TOC gains one more column. This mark is not the Tick rail. A color per level needs new evidence against the channel-map ruling.
Color that never moves the row. Chat-open, scroll-spy, and hover change color only. Depth still comes from the gutter. Trade-off: color alone does not show depth. Do not replace
menu-focuswith a brand wash. Do not add a chat-open accent bar.Fixed chevron box. Paint the chevron in a box that does not change the title column. Trade-off: the icon can cover the first emoji. The grip already sits at
left: -22px. Do not move presence.Darker indent guides. Trade-off: this overturns the stock rail opacity rule. It needs new evidence. Do not add L-elbows.
Acceptance criteria
Blocked by
None.
Agent brief
Type: HITL
Category: enhancement
Current behavior: A parent row renders a fold button. A leaf row collapses the leading slot. The title starts further left. On the measured Wide TOC, that shift was 24px. Chat-open and scroll-spy change color only. The list is a
uloflielements with classmenu. It is not an ARIA tree.Desired behavior: The maintainer picks one direction from the preview. Rows of one heading level then share one start for the icon and the title. State changes do not move that start. Wide TOC and TocModal match. The settled list above still holds.
Where to start:
apps/webapp/src/components/toc/TocRow.tsx(TocRow).apps/webapp/src/components/toc/TocItemBody.tsx(leading,hasChildren).apps/webapp/src/components/toc/TocRowTrail.tsx.apps/webapp/src/styles/components/_tableOfContents.scss.apps/webapp/src/styles/components/_tocDrag.scss(.toc-drag-handle).apps/webapp/src/components/pages/document/components/TocModal.tsx. LeaveTocTickRailalone unless a chosen direction says otherwise. Search by symbol. File names are hints as of 2026-09-29.Rules that apply: Root
CLAUDE.mdholds the Settled list.apps/webapp/CLAUDE.md§TOC And Heading Actions and §Pad Workspace Surfaces apply.CONTEXT.md§Pad outline names Wide TOC and the Tick rail..cursor/docs/design-system.md§Table of contents and §State language apply.AGENTS.mdrequires the visual preview before UI code. Keep theToc*names. Do not rebrand the folder to Outline.Verify: Build the preview first and wait for one approved direction. Then check Wide TOC and TocModal in light, dark, and the premium themes. Parent and leaf rows of one level share one x. Fold, expand, chat-open, scroll-spy, and hover do not move that x. State the keyboard and screen reader behavior and test it. Run the existing TOC Cypress specs. Add one spec that pins the alignment.