You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The miner dashboard redesign adds a persistent chat rail to apps/loopover-miner-ui (mounted once in apps/loopover-miner-ui/src/routes/__root.tsx so it survives route navigation — see the chat rail shell issue for that mounting work). Before the message list, message bubble, and typing indicator that rail needs can be built, this issue delivers them as three standalone, backend-agnostic components, tested only against fixture data — not yet wired to a real backend.
apps/loopover-miner-ui already consumes @loopover/ui-kit as a first-class npm workspace dependency (apps/loopover-miner-ui/package.json:18, "@loopover/ui-kit": ">=0.1.0 <2.0.0") and already imports components straight from the package (e.g. apps/loopover-miner-ui/src/routes/portfolio.tsx:4-6 imports Button/Card/Table from @loopover/ui-kit/components/*). The two primitives this work needs already exist there, unused by any chat-shaped UI today:
packages/loopover-ui-kit/src/components/scroll-area.tsx — the scrollable-viewport primitive the message list must be built on.
packages/loopover-ui-kit/src/components/avatar.tsx — Avatar/AvatarImage/AvatarFallback, the per-message avatar primitive.
#6244's audit (landed as apps/loopover-ui/src/chat-ui-primitives-audit.md via PR #6474) confirmed both are "reusable as-is" for chat, and also confirmed the two things this issue must build are entirely absent from the codebase today: "a chat-bubble/message-list component; a typing/in-progress indicator (no dot-pulse or inline 'other side is composing' affordance — Spinner only signals whole-panel loading)."
That Spinner lives in apps/loopover-ui/src/components/site/state-views.tsx alongside LoadingState/EmptyState/ErrorState/StateBoundary — today it's app-local to apps/loopover-ui, not importable from apps/loopover-miner-ui. #6244 also confirmed StateBoundary is reusable as-is once it exists somewhere both apps can reach; per the redesign synthesis, it must be ported into @loopover/ui-kit proper before any chat component that renders its own async state is built, since the message list is exactly that component. That port is a separate, prerequisite issue (the StateBoundary ui-kit port issue) — this issue is blocked by it.
packages/loopover-ui-kit/src/theme.css already defines the palette (oklch, "Lovable Lime") both apps consume via @import "@loopover/ui-kit/theme.css" (apps/loopover-miner-ui/src/styles.css:5), including --success/--warning/--danger, --chart-1..5, --sidebar-*, and the .dark-class dark-mode mechanism (theme.css:17) — the tokens for role-differentiated bubble colors already exist and should be reused, not invented.
Requirements
⚠️ Read this before starting. These are new files under apps/loopover-miner-ui/src/components/chat/ — not in packages/loopover-ui-kit (that package holds generic shadcn/Radix primitives, not this app's chat composites) and not in apps/loopover-ui (that's the separate maintainer-chat scope, #6230). A PR that adds these components to either of those other locations does NOT resolve this issue.
MessageList must render its scrollable viewport using ScrollArea imported from @loopover/ui-kit/components/scroll-area — not a raw overflow-y-auto div and not a third-party virtualization library.
MessageList must wrap its rendered content in StateBoundary imported from @loopover/ui-kit/components/state-views (the location the StateBoundary ui-kit port issue creates), covering its own loading/empty/error states for the message array it's given.
Do not import StateBoundary/LoadingState/EmptyState/ErrorState from apps/loopover-ui/src/components/site/state-views.tsx — that path is not reachable from apps/loopover-miner-ui.
Do not copy-paste state-views.tsx's contents into apps/loopover-miner-ui as a local duplicate. If the ui-kit port hasn't landed yet, this issue is blocked and should wait for it rather than working around it with a duplicate file.
MessageBubble must render its avatar using Avatar/AvatarImage/AvatarFallback imported from @loopover/ui-kit/components/avatar, a role-colored bubble background built from existing CSS custom properties already defined in packages/loopover-ui-kit/src/theme.css (no new hardcoded hex/oklch color literals), and a rendered timestamp.
TypingIndicator must be visually and semantically distinct from the existing bare Spinner (apps/loopover-ui/src/components/site/state-views.tsx) — an animated composing affordance (e.g. a dot-pulse), not a reused/re-styled spinner — and must expose an aria-label (or equivalent accessible name) that describes composing/typing, not loading, per research: audit ui-kit for existing message/chat-adjacent UI primitives #6244's finding that Spinner only communicates panel-loading today.
Define a shared fixture message shape (id, role, content, timestamp, and whatever optional avatar fields MessageBubble needs) and a fixtures module under apps/loopover-miner-ui/src/components/chat/ supplying representative sample arrays: empty, a single message, a multi-turn exchange, and at least one long-content edge case.
None of the three components may perform a network call, open a WebSocket/EventSource, or otherwise depend on a live backend — every prop they take is data (a message array, a boolean composing flag, etc.), supplied by fixtures in this issue and by a real data source only in later, separately-scoped issues.
Reuse the existing .dark-class dark-mode mechanism (no component-local light/dark branching, no new theme toggle logic — that's out of scope here).
apps/loopover-miner-ui/src/components/chat/fixtures.ts (or .tsx, if it also needs to render JSX) with the sample message arrays described above
Component tests for all three, exercising the fixture-driven loading/empty/error/populated states of MessageList, the avatar-image-vs-fallback and role-color branches of MessageBubble, and the accessible-name/visual-state of TypingIndicator
Test Coverage Requirements
apps/loopover-miner-ui is under apps/**, which is outside Codecov's coverage.include — only src/** (the ORB Worker app) is measured by Codecov's patch-coverage gate (per this repo's CLAUDE.md). Codecov's codecov/patch check will not score these files, and that's expected, not a gap to work around — don't add these paths to coverage.include as part of this issue.
That said, the local gate (npm run test:ci) still runs and still needs to pass, and this codebase's own testing discipline still applies regardless of what Codecov scores: write tests that exercise every branch of the new components, not just a happy path —
MessageList's three StateBoundary branches (loading, empty, error) plus the populated-list render path
MessageBubble's avatar-image-present vs. avatar-fallback branch, and each role's color branch
TypingIndicator's rendered/hidden (or composing=true/false) branches and its accessible name
A regression test isn't applicable here (no existing bug being fixed) — the invariant/branch tests above are the equivalent bar for new components per this repo's house convention.
Expected Outcome
apps/loopover-miner-ui gains three new, independently tested, backend-agnostic chat UI components — a message list built on the shared ScrollArea primitive with real loading/empty/error handling via the newly-shared StateBoundary, a message bubble pairing avatar + role color + timestamp for the first time in this codebase, and a typing indicator that reads as "someone is composing" rather than "this panel is loading." The chat rail shell issue can compose these directly once it lands, without needing to build any of this scaffolding itself.
Context
The miner dashboard redesign adds a persistent chat rail to
apps/loopover-miner-ui(mounted once inapps/loopover-miner-ui/src/routes/__root.tsxso it survives route navigation — see the chat rail shell issue for that mounting work). Before the message list, message bubble, and typing indicator that rail needs can be built, this issue delivers them as three standalone, backend-agnostic components, tested only against fixture data — not yet wired to a real backend.apps/loopover-miner-uialready consumes@loopover/ui-kitas a first-class npm workspace dependency (apps/loopover-miner-ui/package.json:18,"@loopover/ui-kit": ">=0.1.0 <2.0.0") and already imports components straight from the package (e.g.apps/loopover-miner-ui/src/routes/portfolio.tsx:4-6importsButton/Card/Tablefrom@loopover/ui-kit/components/*). The two primitives this work needs already exist there, unused by any chat-shaped UI today:packages/loopover-ui-kit/src/components/scroll-area.tsx— the scrollable-viewport primitive the message list must be built on.packages/loopover-ui-kit/src/components/avatar.tsx—Avatar/AvatarImage/AvatarFallback, the per-message avatar primitive.#6244's audit (landed as
apps/loopover-ui/src/chat-ui-primitives-audit.mdvia PR #6474) confirmed both are "reusable as-is" for chat, and also confirmed the two things this issue must build are entirely absent from the codebase today: "a chat-bubble/message-list component; a typing/in-progress indicator (no dot-pulse or inline 'other side is composing' affordance —Spinneronly signals whole-panel loading)."That
Spinnerlives inapps/loopover-ui/src/components/site/state-views.tsxalongsideLoadingState/EmptyState/ErrorState/StateBoundary— today it's app-local toapps/loopover-ui, not importable fromapps/loopover-miner-ui. #6244 also confirmedStateBoundaryis reusable as-is once it exists somewhere both apps can reach; per the redesign synthesis, it must be ported into@loopover/ui-kitproper before any chat component that renders its own async state is built, since the message list is exactly that component. That port is a separate, prerequisite issue (the StateBoundary ui-kit port issue) — this issue is blocked by it.packages/loopover-ui-kit/src/theme.cssalready defines the palette (oklch, "Lovable Lime") both apps consume via@import "@loopover/ui-kit/theme.css"(apps/loopover-miner-ui/src/styles.css:5), including--success/--warning/--danger,--chart-1..5,--sidebar-*, and the.dark-class dark-mode mechanism (theme.css:17) — the tokens for role-differentiated bubble colors already exist and should be reused, not invented.Requirements
apps/loopover-miner-ui/src/components/chat/message-list.tsxexportingMessageListapps/loopover-miner-ui/src/components/chat/message-bubble.tsxexportingMessageBubbleapps/loopover-miner-ui/src/components/chat/typing-indicator.tsxexportingTypingIndicatorMessageListmust render its scrollable viewport usingScrollAreaimported from@loopover/ui-kit/components/scroll-area— not a rawoverflow-y-autodiv and not a third-party virtualization library.MessageListmust wrap its rendered content inStateBoundaryimported from@loopover/ui-kit/components/state-views(the location the StateBoundary ui-kit port issue creates), covering its own loading/empty/error states for the message array it's given.StateBoundary/LoadingState/EmptyState/ErrorStatefromapps/loopover-ui/src/components/site/state-views.tsx— that path is not reachable fromapps/loopover-miner-ui.state-views.tsx's contents intoapps/loopover-miner-uias a local duplicate. If the ui-kit port hasn't landed yet, this issue is blocked and should wait for it rather than working around it with a duplicate file.MessageBubblemust render its avatar usingAvatar/AvatarImage/AvatarFallbackimported from@loopover/ui-kit/components/avatar, a role-colored bubble background built from existing CSS custom properties already defined inpackages/loopover-ui-kit/src/theme.css(no new hardcoded hex/oklch color literals), and a rendered timestamp.TypingIndicatormust be visually and semantically distinct from the existing bareSpinner(apps/loopover-ui/src/components/site/state-views.tsx) — an animated composing affordance (e.g. a dot-pulse), not a reused/re-styled spinner — and must expose anaria-label(or equivalent accessible name) that describes composing/typing, not loading, per research: audit ui-kit for existing message/chat-adjacent UI primitives #6244's finding thatSpinneronly communicates panel-loading today.MessageBubbleneeds) and a fixtures module underapps/loopover-miner-ui/src/components/chat/supplying representative sample arrays: empty, a single message, a multi-turn exchange, and at least one long-content edge case.WebSocket/EventSource, or otherwise depend on a live backend — every prop they take is data (a message array, a boolean composing flag, etc.), supplied by fixtures in this issue and by a real data source only in later, separately-scoped issues..dark-class dark-mode mechanism (no component-local light/dark branching, no new theme toggle logic — that's out of scope here).Deliverables
apps/loopover-miner-ui/src/components/chat/message-list.tsxapps/loopover-miner-ui/src/components/chat/message-bubble.tsxapps/loopover-miner-ui/src/components/chat/typing-indicator.tsxapps/loopover-miner-ui/src/components/chat/fixtures.ts(or.tsx, if it also needs to render JSX) with the sample message arrays described aboveMessageList, the avatar-image-vs-fallback and role-color branches ofMessageBubble, and the accessible-name/visual-state ofTypingIndicatorTest Coverage Requirements
apps/loopover-miner-uiis underapps/**, which is outside Codecov'scoverage.include— onlysrc/**(the ORB Worker app) is measured by Codecov's patch-coverage gate (per this repo's CLAUDE.md). Codecov'scodecov/patchcheck will not score these files, and that's expected, not a gap to work around — don't add these paths tocoverage.includeas part of this issue.That said, the local gate (
npm run test:ci) still runs and still needs to pass, and this codebase's own testing discipline still applies regardless of what Codecov scores: write tests that exercise every branch of the new components, not just a happy path —MessageList's threeStateBoundarybranches (loading, empty, error) plus the populated-list render pathMessageBubble's avatar-image-present vs. avatar-fallback branch, and each role's color branchTypingIndicator's rendered/hidden (or composing=true/false) branches and its accessible nameA regression test isn't applicable here (no existing bug being fixed) — the invariant/branch tests above are the equivalent bar for new components per this repo's house convention.
Expected Outcome
apps/loopover-miner-uigains three new, independently tested, backend-agnostic chat UI components — a message list built on the sharedScrollAreaprimitive with real loading/empty/error handling via the newly-sharedStateBoundary, a message bubble pairing avatar + role color + timestamp for the first time in this codebase, and a typing indicator that reads as "someone is composing" rather than "this panel is loading." The chat rail shell issue can compose these directly once it lands, without needing to build any of this scaffolding itself.Links & Resources
apps/loopover-ui/src/chat-ui-primitives-audit.md, the audit this issue's scope is drawn from (confirmsscroll-area.tsx/avatar.tsxreusable as-is,StateBoundaryreusable once ported, and that message-list/bubble/typing-indicator are entirely new builds)packages/loopover-ui-kit/src/components/scroll-area.tsxpackages/loopover-ui-kit/src/components/avatar.tsxapps/loopover-ui/src/components/site/state-views.tsx— current (app-local) home ofLoadingState/EmptyState/ErrorState/StateBoundary/Spinnerpackages/loopover-ui-kit/src/theme.css— token source for role-colored bubbles and the.dark-class mechanismapps/loopover-miner-ui/src/routes/__root.tsx— where the future chat rail shell will mount and consume these componentsapps/loopover-miner-ui/package.json:18— existing@loopover/ui-kitworkspace dependency__root.tsxmount issue — downstream consumer of these components (not a blocker)