Skip to content

ui-kit: port state-views.tsx (LoadingState/EmptyState/ErrorState/StateBoundary) into @loopover/ui-kit #6506

Description

@JSONbored

Context

apps/loopover-ui/src/components/site/state-views.tsx is the app's shared loading/empty/error primitive set. It exports five things: Spinner, LoadingState, EmptyState, StateActionButton, ErrorState, StateBoundary (a private Shell helper and NETWORK_ERROR_KINDS constant back them internally), plus a sixth, unrelated export usePreviewDataState. StateBoundary is the composite: given isLoading/isError/isEmpty, it renders LoadingState, ErrorState, or EmptyState and otherwise passes through children — every route/panel in the app that needs a consistent async-state surface reaches for it instead of hand-rolling its own branches.

#6244's audit (apps/loopover-ui/src/chat-ui-primitives-audit.md, landed via PR #6474, merged 2026-07-16) confirmed these four — LoadingState/EmptyState/ErrorState/StateBoundary — are reusable as-is for the miner-dashboard chat rail's message list, which needs its own async-state surface rather than inventing a second one. That audit also confirmed the outer shell/building-block primitives (this file, plus scroll-area.tsx, avatar.tsx, textarea.tsx/input.tsx/button.tsx) are the only reusable pieces for chat — the message list, composer, streaming renderer, and typing indicator all still need to be built new, in later issues. Per the design synthesis for the miner-dashboard redesign, this port is the one hard prerequisite that has to land before the message-list issue can start, since chat is meant to consume this exact primitive.

@loopover/ui-kit (packages/loopover-ui-kit/) is the shared package already consumed by both apps/loopover-ui and apps/loopover-miner-ui as a real npm workspace dependency — not a fork. apps/loopover-miner-ui already imports ui-kit components directly (e.g. apps/loopover-miner-ui/src/routes/portfolio.tsx:4-6 imports Button/Card/Table from @loopover/ui-kit/components/*), while apps/loopover-ui re-exports through thin local shims under @/components/ui/* — e.g. apps/loopover-ui/src/components/ui/skeleton.tsx, whose entire body is export * from "@loopover/ui-kit/components/skeleton";, backed by packages/loopover-ui-kit/src/components/skeleton.tsx. That's the precedent this move follows, though (see Requirements) state-views.tsx needs one adaptation a pure skeleton.tsx-style shim doesn't.

The one real wrinkle: state-views.tsx currently imports notifyApiFailure and type ApiFailureKind from apps/loopover-ui/src/lib/api/request.ts (request.ts:15 defines ApiFailureKind, request.ts:109 defines notifyApiFailure — an app-local API-status/toast singleton wired to that file's own beginRequest/endRequest/reportApiFailure tracking, used to surface a deduped "Retry" toast when StateBoundary's errorLabel prop is set). apps/loopover-miner-ui/src has no equivalent module at all (confirmed: no notifyApiFailure/ApiFailureKind anywhere in that app), and packages/loopover-ui-kit must never import from an app (that's backwards — apps depend on ui-kit, not the reverse; also, none of apps/loopover-ui's @/... path aliases resolve inside packages/loopover-ui-kit, which has no aliases configured in packages/loopover-ui-kit/tsconfig.json). This is the one place the port needs real (small, additive) adaptation rather than a byte-for-byte copy — spelled out exactly in Requirements below.

Also confirmed while researching this move: Spinner and StateActionButton (two of the file's other exports) have real call sites today outside state-views.tsx itself — audit-feed.tsx, dead-letter-queue-panel.tsx, refresh-meta.tsx, app-panels/rees-analyzer-field-group.tsx, routes/app.operator.tsx, and routes/app.analytics.tsx all import one or both directly. The back-compat re-export has to cover all six of the file's current exports, not just the four named in this issue's title, or those six files break.

Requirements

⚠️ Read this before starting. This issue touches exactly two files:
packages/loopover-ui-kit/src/components/state-views.tsx (new) and
apps/loopover-ui/src/components/site/state-views.tsx (rewritten in place into a back-compat
wrapper — never deleted). A PR that (a) copies the component into ui-kit but leaves the app's
original file untouched or deletes it outright instead of turning it into the wrapper described
below, (b) drops or renames any of the file's six existing exports (Spinner, LoadingState,
EmptyState, StateActionButton, ErrorState, StateBoundary) from the app's existing
@/components/site/state-views import path, or (c) has the ui-kit copy import anything from
apps/loopover-ui (including @/lib/api/request, @/lib/utils, or any other @/... alias — none
of those resolve inside packages/loopover-ui-kit) does NOT satisfy this issue and will be closed.

  1. Create packages/loopover-ui-kit/src/components/state-views.tsx containing Spinner, the
    private Shell helper, StateActionButton, LoadingState, EmptyState, ErrorState, and
    StateBoundary, ported from apps/loopover-ui/src/components/site/state-views.tsx verbatim
    except for the two adaptations in Requirements 3 and 4.
  2. The ported file's cn import must be import { cn } from "../utils"; — the same relative
    pattern every other file in packages/loopover-ui-kit/src/components/*.tsx already uses (e.g.
    skeleton.tsx:1, card.tsx:3, badge.tsx:4), not @/lib/utils.
  3. Define export type ApiFailureKind = "timeout" | "network" | "http"; locally inside the ported
    file. Do not import ApiFailureKind from apps/loopover-ui/src/lib/api/request.ts — this union
    is structurally identical to that file's own ApiFailureKind (request.ts:15), so this is a
    zero-logic duplication of a 3-value type alias, not a redesign.
  4. Replace the ported StateBoundary's hardcoded notifyApiFailure(...) call (currently imported
    from apps/loopover-ui/src/lib/api/request.ts:109) with a new optional prop —
    onFailureNotify?: (args: { label: string; kind: ApiFailureKind; message: string; retry?: () => void }) => void
    — invoked inside the exact same if (isError && errorLabel) effect the current code already has,
    with the same { label, kind: errorKind ?? "network", message, retry: onRetry } argument shape.
    When onFailureNotify is not supplied, the effect must no-op. This is the only place any new logic
    is added — everything else in Requirement 1 is a literal move.
  5. Rewrite apps/loopover-ui/src/components/site/state-views.tsx into a back-compat wrapper that:
    re-exports Spinner, LoadingState, EmptyState, StateActionButton, and ErrorState unchanged
    from @loopover/ui-kit/components/state-views; re-exports a StateBoundary that forwards every
    prop through to the ui-kit version but defaults onFailureNotify to the app's real
    notifyApiFailure (from @/lib/api/request) whenever the caller doesn't supply one of its own —
    so every existing app call site's runtime behavior stays byte-identical to before this move; and
    keeps usePreviewDataState defined locally, unmoved (Requirement 7).
  6. Do not change any of these existing import paths — they must keep resolving exactly as they do
    today, with zero edits required in the files themselves: apps/loopover-ui/src/components/site/audit-feed.tsx,
    dead-letter-queue-panel.tsx, refresh-meta.tsx, app-panels/rees-analyzer-field-group.tsx,
    routes/app.operator.tsx, and routes/app.analytics.tsx (all import Spinner and/or
    StateActionButton directly from @/components/site/state-views today).
  7. usePreviewDataState — the file's fifth export, unrelated to the four primitives named in this
    issue's title — has zero call sites anywhere in the app today (confirmed by repo-wide search).
    Leave it exactly where it is, in apps/loopover-ui/src/components/site/state-views.tsx, unmoved
    and unmodified. Do not delete it and do not port it into ui-kit as part of this issue.
  8. Do not modify apps/loopover-ui/src/lib/api/request.ts in this PR.
  9. Do not touch apps/loopover-miner-ui in this PR. Wiring the miner dashboard's header/routes or
    its chat message list to actually consume @loopover/ui-kit's StateBoundary happens in the
    separate issues that depend on this one, not here.
  10. This move produces zero visible/rendered output change in apps/loopover-ui — every prop and
    default the app's callers see stays the same. State that explicitly in the PR's ## Summary; the
    ## UI Evidence screenshot table in the PR template does not apply here since there is no
    before/after to capture.

Deliverables

  • packages/loopover-ui-kit/src/components/state-views.tsx — new file, per Requirements 1-4
  • apps/loopover-ui/src/components/site/state-views.tsx — rewritten into the back-compat wrapper
    described in Requirement 5
  • No other source files changed (Requirements 6, 8, 9)

Test Coverage Requirements

packages/loopover-ui-kit/src/** is not in Codecov's measured paths — the root codecov.yml
comment and vitest.config.ts's coverage.include both list only src/**,
packages/loopover-engine/src/**, packages/loopover-miner/lib/**, and one named analyzer file;
packages/loopover-ui-kit/** was never included, the same way apps/** is explicitly excluded. This
move is not gated by the 99% Codecov patch requirement, in either the source or destination file — do
not expect a codecov/patch check to appear for this diff.

The real regression backstop is apps/loopover-ui's own existing suite:
apps/loopover-ui/src/components/site/state-views.test.tsx (13 it(...) cases covering
ErrorState's network-vs-http default copy, StateBoundary's loading/error/empty/skeleton branches,
retry/refresh actions, and — critically — two assertions that mock @/lib/api/request and check
notifyApiFailure is called with the right kind). Every one of those assertions must still pass
after this move, run via npm --prefix apps/loopover-ui run test (or the root npm run test:ci,
which runs it as part of the full suite) — the two notifyApiFailure-mock assertions specifically
verify that Requirement 4's onFailureNotify wiring reaches the app's real notifier by default. If
the new prop name forces a mechanical update to how the test invokes StateBoundary, make only the
minimal edit needed to keep every existing assertion intent-identical — do not weaken or remove any
of the 13 cases.

packages/loopover-ui-kit has no test runner configured today (no vitest config, no *.test.tsx
anywhere in the package, and its package.json only defines build/typecheck scripts) — do not
add one as part of this issue; that's a separate scope decision this port doesn't imply. Instead:

  • packages/loopover-ui-kit's own npm run typecheck (tsc -p tsconfig.json --noEmit) and
    npm run build must both pass clean on the new file.
  • The root npm run typecheck must pass clean across both changed files.
  • Run the full local gate per house rule 3 before opening the PR: npm run test:ci and
    npm audit --audit-level=moderate.

Expected Outcome

  • @loopover/ui-kit ships LoadingState/EmptyState/ErrorState/StateBoundary (plus Spinner
    and StateActionButton) as real package-level primitives with zero hard dependency on
    apps/loopover-ui's API-status plumbing.
  • apps/loopover-ui's visible behavior and every existing call site's runtime output are unchanged.
  • apps/loopover-miner-ui (or any future consumer) can import StateBoundary and friends directly
    from @loopover/ui-kit/components/state-views without inventing its own copy or stubbing out
    notifyApiFailure.
  • The miner-dashboard chat message-list issue is unblocked — per the design synthesis it's required
    to consume this exact primitive rather than build a second one — and so are the visual/ui-kit
    adoption issues for the miner dashboard's header and four routes, which reuse StateBoundary
    straight from @loopover/ui-kit instead of reaching into apps/loopover-ui.

Links & Resources

  • Source file: apps/loopover-ui/src/components/site/state-views.tsx
  • Destination file: packages/loopover-ui-kit/src/components/state-views.tsx (new)
  • App-local dependency being decoupled: apps/loopover-ui/src/lib/api/request.ts (ApiFailureKind
    at line 15, notifyApiFailure at line 109) — read, not edited, by this issue
  • Shim precedent: apps/loopover-ui/src/components/ui/skeleton.tsx +
    packages/loopover-ui-kit/src/components/skeleton.tsx
  • cn import precedent: packages/loopover-ui-kit/src/components/skeleton.tsx:1,
    card.tsx:3, badge.tsx:4
  • Existing external call sites of Spinner/StateActionButton that must keep working unmodified:
    apps/loopover-ui/src/components/site/audit-feed.tsx, dead-letter-queue-panel.tsx,
    refresh-meta.tsx, app-panels/rees-analyzer-field-group.tsx, routes/app.operator.tsx,
    routes/app.analytics.tsx
  • Audit that scoped this move: apps/loopover-ui/src/chat-ui-primitives-audit.md, landed via PR
    docs(ui): audit ui-kit for existing chat-adjacent UI primitives #6474 (docs(ui): audit ui-kit for existing chat-adjacent UI primitives), closing the audit issue
    that requested it
  • Downstream, dependent work (not in scope here): the miner-dashboard chat message-list issue, and
    the visual/ui-kit adoption issues that restyle the miner dashboard's header and its four routes
    (index/run-history/portfolio/ledgers) using StateBoundary/Skeleton/chart/pagination
    primitives from @loopover/ui-kit

Metadata

Metadata

Assignees

No one assigned

    Labels

    gittensor:featureGittensor-scored feature linked to a feature issue — scores a 0.25x multiplier.help wantedExtra attention is needed

    Projects

    Status
    In Progress

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions