Skip to content

Reusable skeleton/empty/error system + mobile/responsive pass across hosted views #24

Open

Description

@harrymove-ctrl

Context

Hosted views use plain-text loaders ("Loading namespaces…" in NamespacesSimplePage, apps/web/src/main.tsx:5938; "Loading directory…" in ShowcasePage, main.tsx:2110) and ad-hoc red error text, with no shared skeleton/empty/error components. Styling is a single ~184KB apps/web/src/styles.css (--cm-* tokens, no Tailwind/shadcn), and mobile/responsive has not been audited. FailureExplainer already exists (used at main.tsx:2115) but isn't a reusable card.

Goal / user story

As a user on any device, I see polished skeletons while loading, helpful empty states with a next-step CTA, and recoverable error cards with Retry — on a layout that works down to phone width — instead of raw text that shifts or dead-ends.

Acceptance criteria

  • A reusable <Skeleton> (tokenized shimmer using --cm-*) is applied to hosted extraction, facts/graph, chat "thinking", the namespaces list, and the share page.
  • One <EmptyState> component (icon + title + CTA) replaces inline strings like "No namespaces yet" (main.tsx:5938).
  • A structured <ErrorCard> with a Retry action replaces raw red error text (e.g. NamespacesAppPage error at main.tsx:5967, ShowcasePage at main.tsx:2112); FailureExplainer is folded into it.
  • Retry re-invokes the relevant fetch (e.g. NamespacesAppPage.refresh(), main.tsx:5969) without a full reload.
  • Responsive pass: nav (appNavItems, main.tsx:1614), the Build console, and the constellation are usable at 360–768px with no horizontal scroll.
  • No new off-palette hex is introduced (extend --cm-*; namespace-memory.css already drifts — don't add to it).

Implementation notes

  • Add components under apps/web/src/components/ (e.g. Skeleton.tsx, EmptyState.tsx, ErrorCard.tsx) plus tokens in styles.css; replace the scattered subEmpty plain-text usages.
  • Wire Retry handlers to existing refresh functions; keep the light-app / dark-graph brand split intact.
  • Mobile: make AppShell nav collapsible; pair with the constellation mobile work.
  • Parts of this (EmptyState/ErrorCard extraction) are good-first-issue sized.

Sui Overflow angle

The Harbor bucket handshake (RESERVE→SIGN→FINALIZE plus the ~3s mirror_missing_grant window) and SEAL decrypt are inherently async with retry-able failures. This exact skeleton + structured-error system is what lets the demo show "creating on-chain bucket…" and graceful retry states instead of raw errors during the Sui-native storage flow.

Dependencies

Standalone and shippable. Enhances the routed namespace console issue and the onboarding-flow issue.

Part of the ContextMEM roadmap (#4) • Sui Overflow build.

Activity

  1. added this to the Sui Overflow milestone on Jun 16, 2026
  2. added
    P1Important: hardens the demo and core product
    ui-uxWeb app UI/UX, accessibility, and responsive design
    good-first-issueWell-scoped task suitable for a new contributor
    on Jun 16, 2026
  3. harrymove-ctrl commented on Jun 19, 2026

    @harrymove-ctrl
    OwnerAuthor

    Implementation Plan

    Approach
    Build three tiny, dependency-free presentational components — Skeleton, EmptyState, ErrorCard — under apps/web/src/components/ui/, backed by a handful of new tokenized CSS classes + error/danger --cm-* tokens in styles.css. ErrorCard absorbs the existing failureHintFor()/FailureExplainer logic (main.tsx:8002 / :8029) and adds a onRetry prop wired to each page's existing fetch (e.g. NamespacesAppPage.refresh() at main.tsx:5969) so recovery happens in-place, no reload. The responsive pass converts the already-half-responsive AppShell nav (currently a horizontal-scroll strip at the 920px breakpoint, styles.css:7881) into a real collapsible drawer + bottom-bar, and kills horizontal overflow on the Build console and the constellation down to 360px. Key decision: keep all pure logic (error classification, skeleton variant→class) in sibling .ts files so they're unit-testable under the existing node-env vitest (no RTL/jsdom is installed).

    Prerequisites & dependencies

    • None blocking. Issue is explicitly "standalone and shippable" (roadmap §4, P1 "Reusable skeleton/loading + consistent empty/error states").
    • No new npm deps. lucide-react (already a dep) supplies icons for EmptyState/ErrorCard. Do not add @testing-library/react/jsdom — vitest runs environment: "node" and include: ["apps/**/*.test.ts"] (vitest.config.ts), so component render tests would force new infra; keep tests on pure .ts helpers.
    • Coordinates with (does not block) two roadmap items: the "wire orphaned NamespacesAppPage" P0 task (the route swap at main.tsx:1575) and the constellation reduced-motion task. This issue touches NamespacesAppPage's error/empty rendering even though it is still unrouted today.
    • Brand constraint from CLAUDE.md + AC Build HarborClient: reserve→sign→finalize bucket, multipart upload, status poll, download #6: brand green is #a8d946; the light-app / dark-graph split must stay intact; do not add hex to components/namespace-memory/namespace-memory.css (it already drifts).

    Step-by-step

    1. Add tokens + shimmer + error/empty CSS to styles.css. In the brand :root block (styles.css:2337–2351, the one with --cm-accent), add danger/empty tokens so nothing introduces off-palette hex per AC Build HarborClient: reserve→sign→finalize bucket, multipart upload, status poll, download #6:

      • --cm-danger: #b42318; --cm-danger-strong: #912018; --cm-danger-bg: #fef3f2; --cm-danger-border: #fda29b; --cm-skeleton-base: rgba(17,20,22,0.06); --cm-skeleton-sheen: rgba(17,20,22,0.12);
        Then append (after the existing keyframes near styles.css:7815): @keyframes cm-shimmer { 0%{background-position:-468px 0} 100%{background-position:468px 0} } and classes .cmSkeleton (gradient using the two skeleton tokens + animation: cm-shimmer 1200ms linear infinite, border-radius: var(--cm-radius, 8px)), .cmSkeleton--text/--line/--card/--circle size variants, .cmEmptyState, .cmErrorCard (replacing .failurePanel colors at styles.css:6395 with the new --cm-danger-* tokens), and .cmErrorCard__retry. Inside @media (prefers-reduced-motion: reduce) (one already exists at styles.css:8365 / 9967) set .cmSkeleton { animation: none }.
    2. Create apps/web/src/components/ui/Skeleton.tsx. Props { variant?: "text"|"line"|"card"|"circle"; width?; height?; count?; className? }. Renders one or more <span className="cmSkeleton cmSkeleton--{variant}"> with inline width/height. Put the variant→className map in a sibling skeleton.ts (skeletonClass(variant)) for unit testing. Add role="status" aria-busy="true" aria-label="Loading" on the wrapper.

    3. Create apps/web/src/components/ui/EmptyState.tsx. Props { icon?: React.ComponentType<{size?:number}>; title: string; description?: string; action?: { label: string; onClick?: ()=>void; to?: string } }. Renders icon + title + optional description + optional CTA (button or react-router Link). Reuse .subEmpty/.panel spacing conventions so it slots into existing <section className="panel"> wrappers.

    4. Create apps/web/src/components/ui/ErrorCard.tsx + error-card.ts. Move failureHintFor() (main.tsx:8002–8027) into error-card.ts as classifyError(message): { code?; hint?; title } (default title "Something went wrong"). ErrorCard props { error: string|Error|null; title?; onRetry?: ()=>void|Promise<void>; retrying?: boolean }; renders the message, the classified hint/code (folding in FailureExplainer), and a Retry button (lucide RefreshCw) when onRetry is set. Keep the old FailureExplainer (main.tsx:8029) as a 3-line wrapper that renders <ErrorCard> for back-compat at its other call sites, or delete after replacing all usages.

    5. Apply to ShowcasePage (main.tsx:2057, render block 2109–2118): replace <p>Loading directory…</p> with <Skeleton variant="card" count={6}> in a .showcaseGrid-shaped wrapper; replace the errorState block (2112–2116, which already calls FailureExplainer) with <ErrorCard error={error} title="Directory unavailable" onRetry={loadDirectory}>; replace the "No public namespaces…" empty (2118) with <EmptyState>. Extract the existing useEffect fetch into a named loadDirectory() so Retry can re-invoke it.

    6. Apply to NamespacesSimplePage (main.tsx:5893): the inline loading/empty at main.tsx:5938 becomes busy ? <Skeleton variant="card" count={4}/> : !namespaces.length ? <EmptyState icon={Database} title="No namespaces yet" description="Build a context package to create one." action={{label:"Go to Build", to:"/app"}}/> : null. Pull the useEffect body into loadNamespaces() and add an ErrorCard path (currently errors are swallowed in the catch at :5905 — add an error state).

    7. Apply to NamespacesAppPage (main.tsx:5943): replace {error ? <div className="error">{error}</div> : null} (main.tsx:6182) with <ErrorCard error={error} onRetry={() => refresh().catch(...)} retrying={!!busy}>; refresh() already exists at main.tsx:5969 and clears error at :5970 — satisfies AC docs: add product roadmap (Harbor storage + Talus on-chain direction) #4 (no reload). Replace the sub-empties at main.tsx:6196/6397/6417 with <EmptyState>. Add <Skeleton> while the initial refresh() (main.tsx:5988) is in flight (introduce a loading boolean around the first load).

    8. Apply to SharePage (main.tsx:1634): replace the "Loading shared context" panel (main.tsx:1712) with skeleton blocks shaped like the share hero/artifact list, and the errorState panel (:1706) with <ErrorCard error={error} title="Share unavailable" onRetry={loadShare}> (loadShare already exists at main.tsx:1668).

    9. Apply to hosted extraction + facts/graph + chat "thinking":

      • Hosted extraction: the Build console progress already uses demoRunPanel (styles.css:5028). Add <Skeleton> placeholders for the results area while a job is queued/running (the result tabs region, buildTabs main.tsx:1623) so the layout doesn't shift when results arrive.
      • Facts/graph: where facts load before the constellation/graph renders, show <Skeleton variant="card"> instead of bare text.
      • Chat: replace the aiChatTyping spinner line at main.tsx:3529 ("ContextMeM is thinking…") with 2–3 shimmering <Skeleton variant="line"> chat bubbles.
    10. Responsive pass — nav (AC Spike: run @mysten/seal encrypt+decrypt inside a Cloudflare Worker #5). In AppShell (main.tsx:2196, nav at :2230) add a hamburger toggle button + useState(open) and an aria-controls/aria-expanded pair; render the existing appNavItems (main.tsx:1614) inside a drawer/bottom-bar. In styles.css, replace the current 920px behavior (styles.css:7881–7942, which makes .appSideNav an overflow-x:auto strip) with: at ≤768px collapse .appSidebar to a top bar + slide-in drawer (or fixed bottom nav using the 6 icons), .appShell { grid-template-columns: 1fr }, and ensure body/.appMain { overflow-x: hidden; min-width: 0 } so there is no horizontal page scroll at 360–768px.

    11. Responsive pass — Build console + constellation (AC Spike: run @mysten/seal encrypt+decrypt inside a Cloudflare Worker #5). Build console: .appWorkspace (styles.css:5217) and the buildTabs row — make tabs wrap (flex-wrap) instead of overflowing, single-column .appWorkspace below 768px (partly done at styles.css:7940). Constellation: in NamespaceMemoryConstellation.tsx + its namespace-memory.css, cap the canvas/container to width:100%; max-width:100vw and make the detail panel a full-width bottom sheet under 768px — without adding new hex to namespace-memory.css (use existing classes / --cm-* only). Verify the 3D canvas doesn't force >100vw.

    12. Unit tests (see Test plan): add skeleton.test.ts and error-card.test.ts next to the helpers.

    New / changed files

    • New: apps/web/src/components/ui/Skeleton.tsx
    • New: apps/web/src/components/ui/skeleton.ts (variant→class helper)
    • New: apps/web/src/components/ui/skeleton.test.ts
    • New: apps/web/src/components/ui/EmptyState.tsx
    • New: apps/web/src/components/ui/ErrorCard.tsx
    • New: apps/web/src/components/ui/error-card.ts (classifyError, moved from failureHintFor)
    • New: apps/web/src/components/ui/error-card.test.ts
    • Changed: apps/web/src/main.tsx — ShowcasePage (2057/2109–2118), SharePage (1668/1706–1712), NamespacesSimplePage (5893/5938), NamespacesAppPage (5969/6182/6196…), chat aiChatTyping (3529), Build results region (~1623), AppShell nav (2196–2244), and refactor failureHintFor/FailureExplainer (8002–8040) to delegate to ErrorCard.
    • Changed: apps/web/src/styles.css — new --cm-danger* / --cm-skeleton-* tokens (in :root at 2337), @keyframes cm-shimmer + .cmSkeleton* / .cmEmptyState / .cmErrorCard* (near 6395/7815), responsive overrides (replace 7881–7942), reduced-motion guard (8365/9967).
    • Changed: apps/web/src/components/namespace-memory/NamespaceMemoryConstellation.tsx + namespace-memory.css — mobile width/bottom-sheet (no new hex).

    Test plan

    • Unit (CI, node env via bun run test → vitest):
      • error-card.test.ts: assert classifyError returns the right code/hint for each branch ("[demo_limit_exceeded]" → DEMO_LIMIT_EXCEEDED, walrus aggregator timeout → WALRUS_UNREACHABLE, memwal 401 → MEMWAL_AUTH, openai 429 → OPENAI_RATE_LIMIT, namespace not found → NAMESPACE_MISSING, read-token → NAMESPACE_TOKEN, unknown → default title, empty → null hint). This is a behavior-preserving port of failureHintFor.
      • skeleton.test.ts: assert skeletonClass(variant) mapping + default.
      • Files must be .test.ts (vitest include excludes .tsx).
    • Typecheck/build: bun run typecheck and bun run build (apps/web vite build) — proves the new .tsx components compile and bundle (bun-check.yml runs bun run check + bun run build).
    • Manual / responsive (proves it works): bun run --filter ./apps/web dev, then drive with chrome-devtools resize_page at 360, 414, 768 and assert via evaluate_script that document.documentElement.scrollWidth <= window.innerWidth (no horizontal scroll) on /app/namespaces, /app (Build), /showcase, and a constellation view. Throttle network (slow 3G) to confirm skeletons render before data; kill the API (/app/namespaces with bad API_BASE) to confirm ErrorCard shows + Retry re-fetches without a full reload (watch network panel: a new XHR fires, no document navigation). Confirm prefers-reduced-motion stops the shimmer (emulate in devtools).

    Risks & gotchas

    • No DOM test runner: keep render-level assertions out of CI; rely on pure-helper unit tests + manual browser checks. Adding RTL/jsdom would expand scope and CI surface — avoid unless requested.
    • NamespacesAppPage is unrouted (route at main.tsx:1575 still uses NamespacesSimplePage). Editing its error/empty rendering (AC Memory knowledge graph: 3D neural constellation + grounded entity panel #2/fix(ci): force bun + pin wrangler in Deploy workflow #3) won't be visible in-app until the separate "wire up" task lands — verify those changes via typecheck/build, not the running app. Coordinate so you don't conflict with that task's edit to the same component.
    • failurePanel is reused at other call sites (e.g. ShowcasePage at main.tsx:2115); if you delete FailureExplainer instead of shimming it, grep every usage first and replace, or CI typecheck breaks.
    • Off-palette regression (AC Build HarborClient: reserve→sign→finalize bucket, multipart upload, status poll, download #6): the existing .failurePanel red hex (#fef2f2/#fecaca/#7f1d1d/#991b1b at styles.css:6395) must be tokenized, not copied; do not introduce hex into namespace-memory.css.
    • Two :root blocks (styles.css:1 and :2337) — add tokens to the brand block at :2337 (the one defining --cm-accent), not the pre-brand block at :1.
    • Constellation overflow: the three.js canvas can set an explicit pixel width that exceeds the viewport — clamp container to 100%/max-width:100vw and re-measure on resize, else AC Spike: run @mysten/seal encrypt+decrypt inside a Cloudflare Worker #5 "no horizontal scroll" fails.
    • Bundle/perf: components are trivial; main.tsx is already 8.2k lines — these edits are additive and shouldn't worsen the monolith, but do not attempt the route-splitting refactor here (separate roadmap item).
    • Sui Overflow tie-in (informational): this skeleton + ErrorCard.onRetry system is the substrate for showing "creating on-chain bucket…" and retrying the ~3s mirror_missing_grant window during the Harbor flow — design the skeleton copy/retry to be reusable for that, but no Harbor code in this issue.

    Acceptance-criteria mapping

    • Reusable <Skeleton> on hosted extraction, facts/graph, chat thinking, namespaces list, share page → Steps 1–2, 5, 6, 8, 9.
    • One <EmptyState> replaces inline strings like "No namespaces yet" (5938) → Steps 3, 5 (showcase empty), 6 (main.tsx:5938), 7 (6196/6397/6417).
    • Structured <ErrorCard> with Retry replaces raw red text (NamespacesAppPage 5967, ShowcasePage 2112); FailureExplainer folded in → Steps 4, 5, 7 (replaces <div className="error"> at main.tsx:6182), 8.
    • Retry re-invokes the relevant fetch (refresh(), 5969) without full reload → Steps 4, 7 (onRetry={() => refresh()}), 5/6/8 (loadDirectory/loadNamespaces/loadShare); verified in Test plan manual section.
    • Responsive nav (appNavItems 1614), Build console, constellation usable 360–768 no horizontal scroll → Steps 10, 11; verified via chrome-devtools scrollWidth assertion.
    • No new off-palette hex (extend --cm-*; don't touch namespace-memory.css) → Step 1 (tokens) + Step 11 constraint; no hex added to namespace-memory.css.

    Effort
    M, ~2.5–3.5 person-days. Components + tokens + tests ≈ 1 day; wiring across the 5+ call sites in the 8.2k-line main.tsx ≈ 1 day; responsive nav/Build/constellation pass + cross-device verification ≈ 0.5–1.5 days. (EmptyState/ErrorCard extraction alone is good-first-issue sized per the issue's note.)


    Implementation plan generated for execution (planning phase). Part of the roadmap (#4).

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    P1Important: hardens the demo and core productgood-first-issueWell-scoped task suitable for a new contributorui-uxWeb app UI/UX, accessibility, and responsive design

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions