Skip to content

refactor(loading): unify every placeholder on one shared skeleton primitive - #1281

Open
Harry19081 wants to merge 1 commit into
developfrom
refactor/loading-skeleton-sweep
Open

refactor(loading): unify every placeholder on one shared skeleton primitive#1281
Harry19081 wants to merge 1 commit into
developfrom
refactor/loading-skeleton-sweep

Conversation

@Harry19081

@Harry19081 Harry19081 commented Sep 4, 2026

Copy link
Copy Markdown
Member

Problem

#1280 landed the static-placeholder primitive and converted the GitHub detail
and inbox/list surfaces. Two gaps remained.

The primitive's name collides. #1280 shipped it as Ghost / GhostBar.
Ghost is already taken twice in this repository — GHOST_PILL_* in
PillGroup for a low-emphasis pill surface, and ReferenceDragGhost for a drag
preview — while skeleton is already this repo's word for exactly this pattern
(GitHubDetailSkeleton, TimelineLoadingSkeleton, McpTableSkeleton, the
detail-tab-count-skeleton test id). The rename was meant to be part of #1280,
but that PR was merged before the amend landed, so it is folded in here — while
the primitive still has only three import sites rather than nine.

Six hand-rolled placeholders were left behind, and they disagreed with each
other on two axes:

  • They still animated. All six used animate-pulse, so the flash that
    fix(loading): replace pulsing skeletons with static skeleton rows #1280 removed from the GitHub surfaces was still present in chat, the canvas
    inline card, two Integrations panels, and the DevTools playground.
  • They disagreed on the fill token. Three used bg-fill-3 and three used
    bg-fill-2 for the same semantic role — a placeholder for content that has
    not arrived. fill-3 is one step darker in both themes (#e3e3e3 vs
    #efefef light, #2e2e30 vs #212121 dark).

Separately, the audit found that one of the six was never on screen at all:
McpTableSkeleton is exported from McpTableParts.tsx and the identifier
appears nowhere else in src/. Both modules that import from that file
(McpTable.tsx:46, AgentMcpSection.tsx:39) pull other members.

Solution

Rename the primitive, then converge every remaining placeholder on it. One
responsibility: exactly one loading-placeholder primitive exists, it is named
the repository's own word for the pattern, and every placeholder uses it.

src/components/Ghost/src/components/Skeleton/ (GhostBar
SkeletonBar), and ListPanelGhostItem.tsxListPanelSkeletonRows.tsx
(ListPanelGhostListListPanelSkeletonRows, test ids
list-panel-ghost-*list-panel-skeleton-*). Rename-only across the five
consumers #1280 touched; no behavior change in that half of the diff.

Then convert the five live placeholders to SkeletonBar and delete the dead
one.

Site Change
src/components/ChatLoadingBlock.tsx SkeletonBar, keeps its 900px width token
src/engines/ChatPanel/blocks/primitives/ChatLoadingBlock.tsx SkeletonBar, keeps its 800px width token
src/engines/ChatPanel/blocks/CanvasInlineCard/index.tsx 5 bars → SkeletonBar, fill-3fill-2
src/modules/MainApp/Integrations/RulesMemoryEvolution/Evolution/AgentEvolutionPanel.tsx SkeletonBar, fill-3fill-2
src/modules/MainApp/Integrations/DevTools/playground/previews/EventRenderer.tsx SkeletonBar
src/modules/MainApp/Integrations/Mcp/Table/McpTableParts.tsx deleted — dead export, plus its orphaned SKELETON_ROW_COUNT and empty section banner

SkeletonBar gains one prop: testId. ChatLoadingBlock must keep the
chat-loading-block test id that three suites assert, and threading it through
the primitive avoids wrapping it in a <div> whose only job is to carry an
attribute.

On the fill-3fill-2 unification: there is no evidence the darker value
was deliberate. AgentEvolutionPanel's placeholder stands in for a Switch,
and Switch styles its track through the SCSS class .switch-track, not a
Tailwind fill token — so the placeholder was never matched to the control's
real color to begin with. Seven sites already used fill-2.

Resulting invariant, now repository-wide: a loading placeholder is a static
SkeletonBar filled with bg-fill-2.
Animated indicators — spinners,
streaming dots, the CI-pending heartbeat — are a different affordance and are
explicitly out of scope; docs/frontend-ui-audit-2026-09-05/GLOBAL.md lists
each one as keep with reason so the next audit pass does not re-flag them.

Potential risks

  • Three sites get a lighter placeholder. The fill-3fill-2 change is
    the only user-visible delta beyond the removed animation, and it is one step
    lighter in both themes. If a reviewer decides the canvas card wanted more
    contrast against its surface, the revert is a className on those five bars —
    but note that reintroducing it restores the drift this PR removes.
  • McpTable now has no loading placeholder in the source at all. That is
    not a behavior change — the deleted component rendered nowhere — but anyone
    who assumed the table had a loading state will find it does not. Giving it one
    is a feature, not part of this sweep.
  • ChatLoadingBlock renders a <span> instead of a <div>. SkeletonBar
    is a span so it stays valid inside phrasing containers. Both blocks sit in
    block context where display:block on a span is equivalent, and both suites
    were updated from a <div> regex to a <span> one. A caller relying on
    div in a CSS selector would break; none do.
  • testId widens the primitive's API. It renders as an omitted attribute
    when unset, so no existing markup changes.
  • The two ChatLoadingBlock variants are still two components. Not merged
    on purpose — see the watch row in docs/frontend-ui-audit-2026-09-05/ChatLoadingBlock.md.
    The 900px/800px split is documented and deliberate (detailPanelTokens.ts:75),
    two occurrences is below the 3+ abstract threshold, and merging costs ~12
    import-site edits.
  • The rename touches five files fix(loading): replace pulsing skeletons with static skeleton rows #1280 just changed. It is mechanical
    (identifier and test-id substitution) and the full suite plus tsgo cover it,
    but it does mean this diff is larger than the sweep alone. Reviewing the
    rename half by identifier is faster than by hunk.
  • A blanket substitution is exactly how this can go wrong. It did once: the
    first pass rewrote an unrelated appearance="ghost" Button prop in
    ActivityTimeline to appearance="skeleton". tsgo caught it and it was
    restored; it never reached develop, and develop's copy is verified intact.
    Every remaining occurrence of "skeleton" in the touched files was then read
    back by hand.
  • No dependency, schema, IPC, persistence, or public-API change.

Verification

  • npx vitest run --config config/vitest.config.ts1489 of 1490 files and
    11200 of 11202 tests passed.
    The two failures are
    src/modules/WorkStation/shared/StatusBar/__tests__/CiStatusMenu.test.ts,
    which is not part of this PR: CiStatusMenu.tsx is modified in the shared
    local checkout by a concurrent session, the file has zero overlap with
    anything changed here, and the suite passes in isolation
    (npx vitest run --config config/vitest.config.ts CiStatusMenu → 2/2). CI on
    this branch runs against the committed version of that file.
  • npx tsgo --noEmit --pretty false — clean, exit 0. It caught a real defect
    mid-change: a blanket ghostskeleton rename in fix(loading): replace pulsing skeletons with static skeleton rows #1280 had clobbered an
    unrelated appearance="ghost" Button prop in ActivityTimeline. Restored.
  • npx oxlint -c .oxlintrc.json --max-warnings 0 over every touched directory —
    clean.
  • npx prettier --check on all changed files — clean.
  • Targeted suites for the swept surfaces — 19 files / 134 tests passed:
    ChatLoadingBlock (both), ActivityRouter, ActivityRouter.canvasInline,
    ChatTranscript, CanvasInlineCard (13 suites), ListPanelSkeletonRows.
  • Both ChatLoadingBlock suites were rewritten to assert the absence of
    animate-pulse, the retained width token, and the retained test id.
  • git show origin/develop:src/modules/shared/components/ActivityTimeline/index.tsx
    — confirmed appearance="ghost" is intact on develop, i.e. the substitution
    slip described above never shipped.

Not run: no visual evidence. This is a Tauri desktop app and screen capture is
denied to the agent on this machine. The fill-3fill-2 delta is the one
change a screenshot would have settled, so exact values for both themes are
given under Potential risks for a reviewer to judge, and the swept surfaces
(chat transcript, canvas card, Integrations → Rules/Memory, MCP table) are worth
one pass by eye.

Audit

frontend-ui-audit run over the changed files per .claude/CLAUDE.md routing;
reports in docs/frontend-ui-audit-2026-09-05/ (GLOBAL.md plus one file per
component).

Verdict totals: 7 fix, 6 keep with reason, 1 abstract (landed),
2 watch.

All 7 fixes are in this PR. Deferred, with reasons recorded in the reports:

  • CanvasInlineCard/index.tsx:59 has a hardcoded English aria-label="Loading canvas content" — a real finding, but an i18n change needing a key across 14
    locales, which is a different responsibility from a placeholder sweep.
  • MarkdownLocalImage.tsx:219 and ChatImageThumbnail/index.tsx:118 are
    character-identical pulsing image icons — watch, pending a third occurrence.

The reports are included here rather than split into a docs-only PR because they
explain this PR's single change and nothing else.

🤖 Generated with Claude Code

…mitive

PR #1280 landed the static-placeholder primitive as `Ghost`/`GhostBar` and
converted the GitHub detail and inbox/list surfaces. Two gaps remained.

The name collides. `Ghost` is already taken twice in this repository —
`GHOST_PILL_*` in `PillGroup` for a low-emphasis pill surface, and
`ReferenceDragGhost` for a drag preview — while "skeleton" is already this
repo's word for exactly this pattern (`GitHubDetailSkeleton`,
`TimelineLoadingSkeleton`, the `detail-tab-count-skeleton` test id).
Rename the module and its list wrapper to `Skeleton` / `SkeletonBar` and
`ListPanelSkeletonRows` before the name spreads further.

Six hand-rolled placeholders were left behind. They still animated with
animate-pulse and disagreed on their fill token, three using `bg-fill-3`
where seven used `bg-fill-2` for the same role. Convert the five live ones
to `SkeletonBar` and unify the fill.

Delete `McpTableSkeleton` rather than convert it: it was exported from
`McpTableParts` and imported by nobody, so it rendered nowhere.

Add a `testId` passthrough to `SkeletonBar` so `ChatLoadingBlock` keeps the
`chat-loading-block` test id that three suites assert, without wrapping the
primitive in a div that exists only to carry an attribute.
@Harry19081
Harry19081 force-pushed the refactor/loading-skeleton-sweep branch from ed468f1 to 5566106 Compare September 4, 2026 19:22
@Harry19081
Harry19081 changed the base branch from fix/loading-ghost-rows to develop September 4, 2026 19:22
@Harry19081 Harry19081 changed the title refactor(loading): sweep remaining placeholders onto the shared skeleton refactor(loading): unify every placeholder on one shared skeleton primitive Sep 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant