Skip to content

Keep pad skeleton bones inside the sheet and match the TOC outline #463

Closed

Description

@HMarzban

Summary

The pad loading skeleton has two defects on desktop. The maintainer reported both:

"the loading skeleton in the document page is not cohesive and right, let's see the document skeleton, it is out of the background #fff, and the background not contain all skeleton lines."

"also let's look at the TOC skeleton, it is not mature and it is not like what we have in the editor skeleton."

  1. The document bones paint below the white sheet. This started on 2026-10-01, when the pad main became a flex column.
  2. The TOC skeleton is a stack of filled slabs. The real outline is plain text rows with nest rails, and the editor skeleton uses thin text lines. The two skeletons do not look like one product.

Two smaller mismatches on the same page are in scope, because each one moves pixels at the skeleton-to-pad swap.

Paths are under apps/webapp/src/ unless shown in full.

1. Document bones overflow the sheet

Where

  • apps/webapp/src/components/skeleton/EditorContentSkeleton.tsx:9: the skeleton root carries both ProseMirror and tiptap__editor on one node, so .pad .editor .tiptap__editor paints it as the white sheet.
  • apps/webapp/src/styles/_blocks.scss:35-52: the sheet rule sets height: max-content and margin-bottom: 2rem. :92: .editorWrapper .ProseMirror sets min-height: 800px. Both rules are unlayered, so the skeleton's Tailwind h-full loses to them.
  • components/pages/document/components/DesktopEditor.tsx:30: the pad main is flex-col items-center since 2026-10-01. Before, it was a row.
  • _blocks.scss:18-29: main has an ::after spacer with flex-shrink: 0 and height: calc(300px + var(--chat-panel-height)).

Why it overflows

  • In the column, the sheet is a flex item with the default flex-shrink: 1. The explicit min-height: 800px turns off the automatic "never shrink below content" minimum.
  • So the skeleton sheet shrinks to fit beside the spacer, and the bones keep their height and spill out.
  • The real editor nests the two classes: div.tiptap__editor holds .ProseMirror (EditorContent.tsx:55-64). The 800px minimum lands on the child, and the real sheet cannot shrink.
  • The pre-mount SlugPageLoader (S0) still uses the old row (SlugPageLoader.tsx:71), so S0 holds every bone. The in-layout skeleton (S1) overflows. The S0 to S1 swap therefore moves the sheet's bottom edge, which breaks the skeleton doctrine in apps/webapp/CLAUDE.md §Slug Page Entry And Skeletons.

Fix

  • Add shrink-0 to the root class list in EditorContentSkeleton.tsx:9.
  • The S1 sheet then takes its content height, floored at 800px, like the real sheet and like S0. It has no visible effect in the S0 row, the History row or the mobile column (read from code).
  • Do not touch _blocks.scss or the real editor.

2. TOC skeleton

Where

  • apps/webapp/src/components/skeleton/TableOfContentsLoader.tsx:20-34: every row is skeleton rounded-field h-8, a filled 32px slab.
  • The real row: components/toc/TocRow.tsx:43-65, TocItemBody.tsx:95-114, TocRowTrail.tsx:40-47, TocHeader.tsx:66-79, styled by styles/components/_tableOfContents.scss.

What is wrong (positions read from code, not measured)

  • Shape. A filled slab reads as a selected row. Real rows have no fill at rest. The skeleton visual language says "Text-line bones: bare .skeleton".
  • Size. The bones are 32px tall. The editor sizes a bone to its font (h-4 for 16px body text). TOC titles are 14px.
  • Position. Bones start at x 6, 22 and 38. Real titles start at 54, 70 and 86, after the fold-caret slot.
  • Top inset. pt-2 is applied twice, so every bone sits 8px low.
  • Rails. There are none. The real list draws a rail under each parent.
  • Header. The header shows a chat bone, but the chat trigger is invisible at rest on desktop.
  • Comment. Line 3 says the loader "pixel-locks". It does not.

Fix: one rule. The row frame stays real, and only the text becomes a bone. This is the same rule the sheet border follows around the editor bones.

  • Each row keeps the real box: flex min-h-8 items-center gap-2 px-3 py-1.5. Then a blank caret slot (ms-1 size-5), a text bone (skeleton h-3.5, keeping today's per-row widths such as w-[72%]), and a blank size-6 trail. Keep today's row tree.
  • The header row uses the same row box with an empty first <span>, then skeleton h-4, then a blank size-6 trail. The empty span is the real TocRow leading slot (TocRow.tsx:54). gap-2 still applies after it, so the bone starts at 30px. The trail keeps the real 36px row height. Draw no chat bone. Keep the header wrapper border-b border-base-300 pt-2 pb-1 mb-1 (_tableOfContents.scss:69-79).
  • Each nest group is relative ms-4 before:absolute before:start-2.5 before:inset-y-3 before:w-[var(--border)] before:bg-base-content/10. The rail is a real hairline, not a bone. This matches the daisyUI menu rail (margin-inline-start: 1rem, top and bottom .75rem, opacity: .1) and _tableOfContents.scss:27-33 (nested padding: 0, rail at 10px).
  • Rows stack with no gap. daisyUI menu puts no gap or margin between items, so drop space-y-1 and every mt-1.
  • The list insets mirror the real list: ps-2.5 pe-1.5, and one pt-2.
  • Use utilities, not the real menu markup. _tableOfContents.scss loads late, through document-styles.scss from an effect in _app.tsx. At S0, menu would paint the stock rail and hover fill, then jump.
  • Replace the line 3 comment. Say that the loader copies the geometry by hand because _tableOfContents.scss loads late, and name its sources: _tableOfContents.scss:27-33 and :64-79, and the daisyUI menu row and nest rules.
  • One component serves S0 and S1, so the swap still moves no pixel.

3. Smaller mismatches on the same page

  • Anon header short by two controls. SlugPageLoader.tsx:62-64 draws one bone after Share. The real anon header has History, Theme and Sign in (PadTitle.tsx:114-198). In the anon branch only, add History size-10 rounded-full and Theme size-10 rounded-full before the Sign in bone (rounded-field h-10). The signed-in branch already draws History, bell and avatar (:58-60), so it does not change. Measure the Share and Sign in widths in the browser.
  • Mobile floor flips colour. The SlugPageLoader root, shared by desktop and mobile, is bg-base-200 (SlugPageLoader.tsx:124). The real mobile root (MobileLayout.tsx:50) paints no fill, so the daisyUI page floor --root-bg (base-100) shows. Change it to bg-base-100. Desktop does not change, because the pad children cover the root.

4. Docs

  • Maintainer ruling, 2026-10-07: write the rule into the design system. In .cursor/docs/design-system.md §State language, add one loading (skeleton) row: "The frame stays real, only the text becomes a bone." (maintainer ruling, 2026-10-07, Keep pad skeleton bones inside the sheet and match the TOC outline #463).
  • In the same file, add a loading row to the §TOC rows table. It describes the header row, the heading rows and the nest rails from §2.
  • design-system.md is locked. The ruling covers these edits, but the lock hook still asks the maintainer before the write.
  • In apps/webapp/CLAUDE.md §Slug Page Entry And Skeletons, change "TableOfContentsLoader mirrors the TocHeader row" so it names the header row, the heading rows and the nest rails. In the Skeleton visual language bullet, point to the new §State language row. Do not copy the rule there.

Out of scope

  • Skeletons outside the pad page (chat feed, panels, Filter, Document settings, hyperlink preview): Use one skeleton visual language in chat, panels and popovers #464.
  • The pad header entry animation (PadTitle.tsx:78, doc-region-in). Its catalog row is locked, so it needs its own ruling.
  • A mobile TOC skeleton. TocModal returns null while loading (TocModal.tsx:71-72). Reproduce on a phone first.

Verify

  • Hold S1: block the WebSocket URL in DevTools. The skeleton shows for about 15 s, until SyncErrorCard (read from code).
  • Hold S0: disable JavaScript in DevTools and reload. The server-rendered SlugPageLoader stays on screen.
  • Screenshot each state at the same viewport, then overlay them for the swap checks.

Acceptance criteria

  • Desktop at 1280×800, 1920×1080 and one viewport at least 1440px tall, signed in and signed out: every bone paints inside the white sheet. #pad-main > .tiptap__editor has scrollHeight <= clientHeight.
  • The S0 to S1 swap does not move the sheet's top or bottom edge.
  • TOC: overlay a screenshot of S1 on the loaded TOC. Each bone starts where its real title starts: 30px for the header row, and 54, 70 and 86px for nest levels 1 to 3, from the column edge. Bones sit on the real row lines, and the rails sit under the real rails. No row has a fill.
  • History view, desktop and a mobile user agent: while a version loads, every bone stays inside its frame. HistoryEditorContent.tsx:60 uses the same skeleton.
  • The anon and signed-in headers match the loaded header widths.
  • Mobile user agent: the floor stays base-100 through the swap. There is still no painted sheet on mobile.
  • Repeat the sheet check and the TOC overlay in light, docsplus-dark and docsplus-dark-hc. The rails stay visible, and no bone leaves its frame.
  • The loaded editor and TOC are unchanged: the code diff touches only EditorContentSkeleton.tsx, TableOfContentsLoader.tsx and SlugPageLoader.tsx.
  • .cursor/docs/design-system.md §State language has the ruled loading (skeleton) row, and §TOC rows has a loading row.
  • apps/webapp/CLAUDE.md §Slug Page Entry And Skeletons names the TOC header row, heading rows and rails. Its Skeleton visual language bullet points to the §State language row.

Activity

  1. HMarzban commented on Oct 7, 2026

    @HMarzban
    CollaboratorAuthor

    Maintainer ruling, 2026-10-07

    • Write "the frame stays real, only the text becomes a bone" into the design system's skeleton visual language.
    • The before-and-after preview was shown to the maintainer, and the "after" look is approved.
  2. added 20 commits that reference this issue on Oct 10, 2026
  3. HMarzban commented on Oct 12, 2026

    @HMarzban
    CollaboratorAuthor

    Shipped, 2026-10-10

    Deployed in run 38068638114 (e5e210d02, green).

    • f235f8948: the pad skeleton bones stay inside the sheet, and the TOC skeleton keeps the real 36px row pitch (option A).
    • 03888a3c1: the TOC skeleton nests four levels, with at least two rows per nest.
    • Follow-up cohesion work up to 1cd7dcdfa puts the pad skeleton back in the server HTML (85 bones, up from 0).

    Checked in the browser at 1440×900 in light, dark and dark high contrast, and on a phone width.

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

    DesktopTocTable of contentsUIbugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions