Repository navigation
Keep pad skeleton bones inside the sheet and match the TOC outline #463
Description
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."
- The document bones paint below the white sheet. This started on 2026-10-01, when the pad
mainbecame a flex column. - 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 bothProseMirrorandtiptap__editoron one node, so.pad .editor .tiptap__editorpaints it as the white sheet.apps/webapp/src/styles/_blocks.scss:35-52: the sheet rule setsheight: max-contentandmargin-bottom: 2rem.:92:.editorWrapper .ProseMirrorsetsmin-height: 800px. Both rules are unlayered, so the skeleton's Tailwindh-fullloses to them.components/pages/document/components/DesktopEditor.tsx:30: the padmainisflex-col items-centersince 2026-10-01. Before, it was a row._blocks.scss:18-29:mainhas an::afterspacer withflex-shrink: 0andheight: 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 explicitmin-height: 800pxturns 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__editorholds.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 inapps/webapp/CLAUDE.md§Slug Page Entry And Skeletons.
Fix
- Add
shrink-0to the root class list inEditorContentSkeleton.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.scssor the real editor.
2. TOC skeleton
Where
apps/webapp/src/components/skeleton/TableOfContentsLoader.tsx:20-34: every row isskeleton 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 bystyles/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-4for 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-2is 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 asw-[72%]), and a blanksize-6trail. Keep today's row tree. - The header row uses the same row box with an empty first
<span>, thenskeleton h-4, then a blanksize-6trail. The empty span is the realTocRowleading slot (TocRow.tsx:54).gap-2still applies after it, so the bone starts at 30px. The trail keeps the real 36px row height. Draw no chat bone. Keep the header wrapperborder-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 daisyUImenurail (margin-inline-start: 1rem, top and bottom.75rem,opacity: .1) and_tableOfContents.scss:27-33(nestedpadding: 0, rail at10px). - Rows stack with no gap. daisyUI
menuputs no gap or margin between items, so dropspace-y-1and everymt-1. - The list insets mirror the real list:
ps-2.5 pe-1.5, and onept-2. - Use utilities, not the real
menumarkup._tableOfContents.scssloads late, throughdocument-styles.scssfrom an effect in_app.tsx. At S0,menuwould 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.scssloads late, and name its sources:_tableOfContents.scss:27-33and:64-79, and the daisyUImenurow 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-64draws one bone after Share. The real anon header has History, Theme and Sign in (PadTitle.tsx:114-198). In the anon branch only, add Historysize-10 rounded-fulland Themesize-10 rounded-fullbefore 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
SlugPageLoaderroot, shared by desktop and mobile, isbg-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 tobg-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 oneloading (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
loadingrow to the §TOC rows table. It describes the header row, the heading rows and the nest rails from §2. design-system.mdis 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 "TableOfContentsLoadermirrors the TocHeader row" so it names the header row, the heading rows and the nest rails. In theSkeleton visual languagebullet, 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.
TocModalreturnsnullwhile 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
SlugPageLoaderstays 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__editorhasscrollHeight <= 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:60uses the same skeleton. - The anon and signed-in headers match the loaded header widths.
- Mobile user agent: the floor stays
base-100through the swap. There is still no painted sheet on mobile. - Repeat the sheet check and the TOC overlay in light,
docsplus-darkanddocsplus-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.tsxandSlugPageLoader.tsx. -
.cursor/docs/design-system.md§State language has the ruledloading (skeleton)row, and §TOC rows has aloadingrow. -
apps/webapp/CLAUDE.md§Slug Page Entry And Skeletons names the TOC header row, heading rows and rails. ItsSkeleton visual languagebullet points to the §State language row.
Activity
HMarzban commentedon Oct 7, 2026
Maintainer ruling, 2026-10-07
fix(webapp): match pad skeletons to the loaded pad and unify skeleton…
fix(webapp): show a deeper outline in the TOC skeleton
fix(webapp): end a failed lazy chunk in an error card with Try again
fix(webapp): route spinner states through Loading with no entry fade
chore(webapp): guard skeleton bones, animate-pulse and shimmer keyfra…
fix(webapp): show the phone outline drawer frame while the pad loads
fix(webapp): match panel loaders to the loaded panels
fix(webapp): match settings loaders to the loaded settings
fix(webapp): server-render the pad skeleton and match it to the loade…
fix(webapp): match the chat loaders to the loaded chat
fix(webapp): keep chat media and emoji loaders at their real size
fix(webapp): load history, link pickers and the outline preview in place
fix(admin): speak the house loading language in the dashboard
docs(webapp): record the loading rule and match the loader catalog rows
fix(webapp): end signed-in bones on a failed profile and open setting…
fix(webapp): share the chat busy and breadcrumb hooks
fix(webapp): keep the real frame in history, link and Command jump lo…
fix(webapp): tighten the loading guard and keep the push stats frame …
docs: sync the loading docs with the thermo pass and drop the lock hook
fix(webapp): stop the pad hydration error by loading the shell static…
HMarzban commentedon Oct 12, 2026
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.1cd7dcdfaputs 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.