Skip to content

Show TOC heading depth so rows of one level line up #373

Description

@HMarzban

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

  1. Open a pad on desktop with Wide TOC showing.
  2. Add two headings of one level. Give only the first heading a child heading.
  3. Start both titles with the same emoji.
  4. Compare the emoji start.
  5. Open chat on the parent row. Scroll until the leaf row is the scroll-spy row.
  6. 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.

Do not re-propose

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.

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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.

  6. 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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions