Repository navigation
Use one skeleton visual language in chat, panels and popovers #464
Description
Summary
A read of every skeleton in the webapp found four places where bones break the skeleton visual language. It says: text-line bones are bare .skeleton; control bones use rounded-field; circles use rounded-full; media uses rounded-box; square bones use size-* (apps/webapp/CLAUDE.md §Slug Page Entry And Skeletons). The bones here follow the rule that #463 adds to .cursor/docs/design-system.md §State language: "The frame stays real, only the text becomes a bone."
Several fixes change locked rows in .cursor/docs/design-system.md. The maintainer approved those changes for this PR (ruling, 2026-10-07).
Land after #463. It writes the ruled rule, and it rewrites the TOC loader bones that the rg check below would otherwise match.
Defects
Paths are under apps/webapp/src/.
- Text-line bones drawn as pills.
- These text bones use
rounded-field:components/chatroom/components/skeleton/ChatroomFeedSkeleton.tsx:34,71,72,84,104,components/chatroom/components/skeleton/ChatroomBreadcrumbSkeleton.tsx:13-14,21,23,components/toc/TocTickRail.tsx:425-427,components/settings/components/DocumentMembersRoster.tsx:40-41andcomponents/settings/components/TrashSection.tsx:20-21. Droprounded-fieldfrom each one. - The breadcrumb chevron (
ChatroomBreadcrumbSkeleton.tsx:22,skeleton rounded-field size-2.5) is an icon bone, not a control. Make it bareskeleton size-2.5 shrink-0, as locked row 796 already says. - Docked chat paints next to the bare editor bones, so the two styles show side by side.
components/chatroom/components/MessageCard/components/common/UserReadStatus.tsx:48-49: the icon bone isskeleton ml-2 h-4 w-4 rounded-full p-0, and the "N seen" text bone isrounded-full. The loaded form isIcons.checkDouble, an icon, then text. Change them toskeleton ml-2 size-4andskeleton h-4 w-10.ChatroomFeedSkeleton.tsxMOBILE_ROWShas two info accents (:149,:153). The catalog allows at most one per feed (design-system.md:797). Delete theblockrow at:149, so mobile keeps thepanelaccent thatDESKTOP_ROWSuses.- Locked rows: §Message context menus
read-status footer(line 466 today) and §Chat prose + chatroom skeletonsfeed / composer skeleton(line 796 today).
- These text bones use
- Invisible tab-track bones.
components/skeleton/SidebarLoader.tsx:8-11andcomponents/TipTap/mediaPopovers/MediaInsertPanelSkeleton.tsx:9-12drawskeletonbones on abg-base-300track. The bone and the track are the same colour (1.0:1, computed from theme hex, not measured).components/PanelSurfaceSkeleton.tsx:30-41already has the right recipe: a staticbg-base-100 rounded-field min-h-9active pill on the track.- Fix: move
:30-41into one exportedPanelTabBarSkeleton({ tabCount })inPanelSurfaceSkeleton.tsx. It owns theshrink-0 px-4 py-2.5wrapper and thebg-base-300 rounded-box p-1track with nogap-1, ascomponents/ui/PanelTabBar.tsx:87-91does.PanelSurfaceSkeletonandSidebarLoaderuse it; the media insert loader gets it through §3. - Locked row: §Media insert panel internals
preview / skeleton(line 629 today).
- Chunk-loader headers that do not match the real panel header.
components/TipTap/toolbar/desktop/FilterSkeleton.tsx:5-8: no bordered header, anh-4 w-12 roundedtitle and asize-6 roundedclose. The real panel isPanelSurfaceShell title="Filter"(components/TipTap/toolbar/desktop/FilterPanel.tsx:237). The header is about 23px short (read from code, not measured).components/TipTap/toolbar/desktop/DocumentSettingsSkeleton.tsx:14-15: the close issize-7 rounded, but the real one isCloseButton size=sm(32px,rounded-field). Two text bones areh-8(:28,:32), and four more bones use barerounded(:4,:5,:22,:23).MediaInsertPanelSkeleton.tsx:4-7copies thePanelSurfaceSkeletonheader by hand.- Fix: give
PanelSurfaceSkeletonan optionalchildrenthat replaces its defaultp-3PanelFeedSkeletonbody, and maketabCountoptional (no tab track when absent). It then mirrorsPanelSurfaceShell: frame, bordered header, body. The header stays in one place, including the NotificationsheaderActionbone. FilterSkeleton(titleWidthClassName="w-12"),DocumentSettingsSkeleton("w-36") andMediaInsertPanelSkeleton(defaultw-24,tabCount={2}) render only their body bones inside it. Widths are read from code, not measured.- In
DocumentSettingsSkeleton, each real row is aToggleRow: atext-smlabel over a help line (components/ui/ToggleRow.tsx:32-41). Replace eachh-8bone with aflex flex-col gap-1column ofskeleton h-3.5 w-16overskeleton h-3 w-44. Drop bareroundedfrom the other four bones; it equals the base 0.25rem radius. - Locked rows: §Media insert panel internals
preview / skeleton(line 629 today), §Panel stackchunk loader(line 816 today), and §popoverPanelClassNameconsumers(line 516 today). Row 516 says only Bookmarks and Notifications usePanelSurfaceSkeleton. That is a routine catalog fix, and the lock hook still asks the maintainer.
- A hand-built skeleton.
- The hyperlink preview loader (
styles/document-styles.scss:224,:249-286;components/TipTap/hyperlinkPopovers/previewShared.ts:139-145) rebuilds.skeletonwith a different ink and a 1.2s rhythm (daisyUI uses 1.8s). - Fix: in
createLoadingSkeleton, give the two bones daisyUI classes:skeleton size-[18px] shrink-0 rounded-fullandskeleton h-3 min-w-20 flex-1. Then delete the keyframes (document-styles.scss:224-231) and the bone rules (:254-285). Keep themetadata-loadingrule at:249-252, its closing brace at:286, and themin-width: 0arm at:156-158. - Keep
min-w-20. The bar mounts inside the 200px.metadatarow (components/TipTap/hyperlinkPopovers/previewHyperlink.ts:167), and its parent does not grow. Without the oldmin-width: 80px, an emptyflex-1bar is 0px wide (read from code, not measured). previewShared.tsis webapp code, not the published extension.- Locked row: §Hyperlink popovers
buttons / loading(line 618 today).
- The hyperlink preview loader (
Out of scope
- A per-surface bone tint. It needs a new token.
- The pad page skeleton: Keep pad skeleton bones inside the sheet and match the TOC outline #463.
UserProfileDialogSkeleton's entry fade and theParticipantsListloader.- Body padding in the media and Filter loaders (
px-4 pt-1 pb-4andpx-3 pt-1 pb-3; the real bodies arep-3).
Verify
bun run --filter @docs.plus/webapp typecheck.- In Chrome DevTools, tick Disable cache and set Network to Slow 4G. Reload, then open each surface once: Filter, Document settings and Insert media in the desktop toolbar; the History sidebar; a link preview on a link with no saved title; docked and mobile chat; a message context menu (read status); the Settings members roster and Trash; a TOC tick-rail preview.
- The popover header is 57px: 1px border + 24px padding + 32px close (read from code, not measured). The loader header and the loaded header must both measure 57px.
- Switch the theme to
docsplus-dark-hcin Settings, and repeat the tab-track and hyperlink checks.
Acceptance criteria
- Every bone listed under Defect 1 is bare
.skeleton: norounded-fieldorrounded-full.rg "skeleton[^\"']*rounded-(field|full)"onChatroomFeedSkeleton.tsx,ChatroomBreadcrumbSkeleton.tsx,UserReadStatus.tsx,TocTickRail.tsx,DocumentMembersRoster.tsxandTrashSection.tsxreturns only circles (rounded-fullon asize-*bone), the day chip (ChatroomFeedSkeleton.tsx:15) and the Trash button bone (TrashSection.tsx:23). - Tab-track bones are visible in light, dark and
docsplus-dark-hc. One component,PanelTabBarSkeleton, draws them. - The Filter, Document settings and media insert loaders render inside
PanelSurfaceSkeleton. Their header height and close size match the loaded panels, and the swap does not move the header. - The hyperlink preview loader uses daisyUI
.skeleton, its bar is visible, and the custom keyframes and bone rules are gone. - In light, dark and
docsplus-dark-hc, every changed loader shows visible bones. Check the Filter, Document settings, media insert, History and hyperlink loaders on desktop. Check the chat feed, breadcrumb and read-status loaders on desktop and on a mobile user agent. - The mobile chat feed skeleton shows one info accent.
- The same PR updates the locked rows in
.cursor/docs/design-system.mdto match the new class strings: 466 (read-status loading bones), 516 (all five popover loaders usePanelSurfaceSkeleton), 618 (daisyUIskeletonbones replacehyperlink-preview-shimmer), 629 (media skeleton tab track and header), 796 (bare feed and breadcrumb text bones), and 816 (PanelSurfaceSkeletontakes optionalchildrenandtabCount, andPanelTabBarSkeletondraws the track).
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): 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
HMarzban commentedon Oct 12, 2026
Shipped, 2026-10-10
Deployed in run 38068638114 (
e5e210d02, green).f235f8948: one skeleton language in chat, panels and popovers..cursor/docs/design-system.md§State language, rowloading (skeleton). A lint guard in ESLint and stylelint keeps new loaders on it.apps/webapp/src/components/ui/:TextLine,FieldSkeleton,ToggleRowSkeleton,PanelTabBarSkeleton. Every lazy chunk ends inChunkLoadFallback, so no skeleton can wait forever.Checks: tsc, ESLint, stylelint, Jest 365/365, chatroom Cypress 32/32.