Skip to content

Use one skeleton visual language in chat, panels and popovers #464

Closed

Description

@HMarzban

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

  1. 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-41 and components/settings/components/TrashSection.tsx:20-21. Drop rounded-field from each one.
    • The breadcrumb chevron (ChatroomBreadcrumbSkeleton.tsx:22, skeleton rounded-field size-2.5) is an icon bone, not a control. Make it bare skeleton 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 is skeleton ml-2 h-4 w-4 rounded-full p-0, and the "N seen" text bone is rounded-full. The loaded form is Icons.checkDouble, an icon, then text. Change them to skeleton ml-2 size-4 and skeleton h-4 w-10.
    • ChatroomFeedSkeleton.tsx MOBILE_ROWS has two info accents (:149, :153). The catalog allows at most one per feed (design-system.md:797). Delete the block row at :149, so mobile keeps the panel accent that DESKTOP_ROWS uses.
    • Locked rows: §Message context menus read-status footer (line 466 today) and §Chat prose + chatroom skeletons feed / composer skeleton (line 796 today).
  2. Invisible tab-track bones.
    • components/skeleton/SidebarLoader.tsx:8-11 and components/TipTap/mediaPopovers/MediaInsertPanelSkeleton.tsx:9-12 draw skeleton bones on a bg-base-300 track. The bone and the track are the same colour (1.0:1, computed from theme hex, not measured).
    • components/PanelSurfaceSkeleton.tsx:30-41 already has the right recipe: a static bg-base-100 rounded-field min-h-9 active pill on the track.
    • Fix: move :30-41 into one exported PanelTabBarSkeleton({ tabCount }) in PanelSurfaceSkeleton.tsx. It owns the shrink-0 px-4 py-2.5 wrapper and the bg-base-300 rounded-box p-1 track with no gap-1, as components/ui/PanelTabBar.tsx:87-91 does. PanelSurfaceSkeleton and SidebarLoader use it; the media insert loader gets it through §3.
    • Locked row: §Media insert panel internals preview / skeleton (line 629 today).
  3. Chunk-loader headers that do not match the real panel header.
    • components/TipTap/toolbar/desktop/FilterSkeleton.tsx:5-8: no bordered header, an h-4 w-12 rounded title and a size-6 rounded close. The real panel is PanelSurfaceShell 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 is size-7 rounded, but the real one is CloseButton size=sm (32px, rounded-field). Two text bones are h-8 (:28, :32), and four more bones use bare rounded (:4, :5, :22, :23).
    • MediaInsertPanelSkeleton.tsx:4-7 copies the PanelSurfaceSkeleton header by hand.
    • Fix: give PanelSurfaceSkeleton an optional children that replaces its default p-3 PanelFeedSkeleton body, and make tabCount optional (no tab track when absent). It then mirrors PanelSurfaceShell: frame, bordered header, body. The header stays in one place, including the Notifications headerAction bone.
    • FilterSkeleton (titleWidthClassName="w-12"), DocumentSettingsSkeleton ("w-36") and MediaInsertPanelSkeleton (default w-24, tabCount={2}) render only their body bones inside it. Widths are read from code, not measured.
    • In DocumentSettingsSkeleton, each real row is a ToggleRow: a text-sm label over a help line (components/ui/ToggleRow.tsx:32-41). Replace each h-8 bone with a flex flex-col gap-1 column of skeleton h-3.5 w-16 over skeleton h-3 w-44. Drop bare rounded from the other four bones; it equals the base 0.25rem radius.
    • Locked rows: §Media insert panel internals preview / skeleton (line 629 today), §Panel stack chunk loader (line 816 today), and §popoverPanelClassName consumers (line 516 today). Row 516 says only Bookmarks and Notifications use PanelSurfaceSkeleton. That is a routine catalog fix, and the lock hook still asks the maintainer.
  4. A hand-built skeleton.
    • The hyperlink preview loader (styles/document-styles.scss:224, :249-286; components/TipTap/hyperlinkPopovers/previewShared.ts:139-145) rebuilds .skeleton with 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-full and skeleton h-3 min-w-20 flex-1. Then delete the keyframes (document-styles.scss:224-231) and the bone rules (:254-285). Keep the metadata-loading rule at :249-252, its closing brace at :286, and the min-width: 0 arm at :156-158.
    • Keep min-w-20. The bar mounts inside the 200px .metadata row (components/TipTap/hyperlinkPopovers/previewHyperlink.ts:167), and its parent does not grow. Without the old min-width: 80px, an empty flex-1 bar is 0px wide (read from code, not measured).
    • previewShared.ts is webapp code, not the published extension.
    • Locked row: §Hyperlink popovers buttons / loading (line 618 today).

Out of scope

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-hc in Settings, and repeat the tab-track and hyperlink checks.

Acceptance criteria

  • Every bone listed under Defect 1 is bare .skeleton: no rounded-field or rounded-full. rg "skeleton[^\"']*rounded-(field|full)" on ChatroomFeedSkeleton.tsx, ChatroomBreadcrumbSkeleton.tsx, UserReadStatus.tsx, TocTickRail.tsx, DocumentMembersRoster.tsx and TrashSection.tsx returns only circles (rounded-full on a size-* 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.md to match the new class strings: 466 (read-status loading bones), 516 (all five popover loaders use PanelSurfaceSkeleton), 618 (daisyUI skeleton bones replace hyperlink-preview-shimmer), 629 (media skeleton tab track and header), 796 (bare feed and breadcrumb text bones), and 816 (PanelSurfaceSkeleton takes optional children and tabCount, and PanelTabBarSkeleton draws the track).

Activity

  1. HMarzban commented on Oct 7, 2026

    @HMarzban
    CollaboratorAuthor

    Maintainer ruling, 2026-10-07

    • The locked design-system rows named above may change in the same PR.
    • The before-and-after preview was shown to the maintainer, and the "after" look is approved.
  2. HMarzban commented on Oct 12, 2026

    @HMarzban
    CollaboratorAuthor

    Shipped, 2026-10-10

    Deployed in run 38068638114 (e5e210d02, green).

    • f235f8948: one skeleton language in chat, panels and popovers.
    • The cohesion follow-up audited 377 loading states. The one rule now lives in .cursor/docs/design-system.md §State language, row loading (skeleton). A lint guard in ESLint and stylelint keeps new loaders on it.
    • Shared bones live in apps/webapp/src/components/ui/: TextLine, FieldSkeleton, ToggleRowSkeleton, PanelTabBarSkeleton. Every lazy chunk ends in ChunkLoadFallback, so no skeleton can wait forever.

    Checks: tsc, ESLint, stylelint, Jest 365/365, chatroom Cypress 32/32.

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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions