mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-01 06:42:06 +01:00
feat(ui): add animated paperclip page loaders (#10456)
## Thinking Path > - Paperclip is the open source control plane people use to manage AI agents for work > - Operators regularly pass through full-page loading states while authentication and company context resolve > - Those states currently render as small bare text, which is easy to miss and does not reinforce Paperclip's visual identity > - A shared loading component gives these transitions one accessible, consistent representation > - This pull request introduces an animated paperclip loader and uses it at the existing full-page loading boundaries > - The benefit is a clearer, calmer loading experience with reduced-motion and screen-reader support ## Linked Issues or Issue Description ### Subsystem affected `ui/` — React + Vite board UI ### Problem or motivation Full-page authentication, access-gate, and company-context waits use small bare `Loading…` text that is visually weak and inconsistent. ### Proposed solution Use one large centered paperclip loader at those boundaries, drawing the SVG with `currentColor` so it follows the active theme. ### Alternatives considered Keeping text-only states or adding a generic spinner would preserve less of Paperclip's product identity and would continue duplicating loading markup. ### Roadmap alignment This is tightly scoped UI polish and does not duplicate a planned roadmap capability. ### Additional context Internal coordination task PAP-15760 requested this focused change. ## What Changed - Added `AnimatedPaperclipIcon`, a theme-aware SVG whose stroke draws in a loop. - Added `PaperclipLoading`, a large full-viewport centered loader with `role="status"` and an `sr-only` `Loading…` label. - Added a static fully drawn fallback under `prefers-reduced-motion: reduce`. - Replaced bare loading text in `CloudAccessGate`, the Auth session check, and three company-context redirects. - Used token-safe Tailwind utilities throughout the component. - Added focused coverage for the status semantics and the Auth layout height override. ## Verification - `pnpm check:token-gates` - `pnpm -C ui exec tsc -b` - `pnpm -C ui exec vitest run src/components/AnimatedPaperclipIcon.test.tsx` - `pnpm -C ui exec vitest run src/App.test.tsx src/App.cases-routing.test.tsx` — 8/8 tests passed - Visually checked light and dark loading states; the screenshot below shows both themes.  ## Risks - Low risk: this changes presentation only at existing loading branches. - Motion-sensitive users receive a static, fully drawn paperclip through the reduced-motion media query. - Screen readers retain a concise loading announcement through the status role and visually hidden label. > For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and discuss it in `#dev` before opening the PR. Feature PRs that overlap with planned core work may need to be redirected — check the roadmap first. See `CONTRIBUTING.md`. ## Model Used - OpenAI Codex, GPT-5 (exact service build ID and context-window size are not exposed in this environment), with reasoning, repository inspection, shell tool use, code execution, and image inspection. ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used with all deployment details available to this environment - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have searched GitHub for duplicate or related PRs and found none - [x] I have described the issue in-PR following the feature-request fields - [x] I included the task-mandated internal parent reference and no private instance URL - [x] I preserved the task-mandated existing branch name without renaming it - [x] I have run scoped tests locally and they pass - [x] I added focused component coverage for the new loading state - [x] No documentation update is required for this presentation-only change - [x] I have considered and documented the risks above - [x] All Paperclip CI gates are green - [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [x] I will address all Greptile and reviewer comments before requesting merge Co-authored-by: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
a93a74f91a
commit
24aa2f516d
6 files changed
+111
-6
No files matched your search
@@ -0,0 +1,23 @@
|
||||
// @vitest-environment node
|
||||
|
||||
import { renderToStaticMarkup } from "react-dom/server";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { PaperclipLoading } from "./AnimatedPaperclipIcon";
|
||||
|
||||
describe("PaperclipLoading", () => {
|
||||
it("renders an accessible full-page loading state", () => {
|
||||
const html = renderToStaticMarkup(<PaperclipLoading />);
|
||||
|
||||
expect(html).toContain('role="status"');
|
||||
expect(html).toContain("min-h-dvh");
|
||||
expect(html).toContain('aria-hidden="true"');
|
||||
expect(html).toContain('<span class="sr-only">Loading…</span>');
|
||||
});
|
||||
|
||||
it("allows containing layouts to override the full-page height", () => {
|
||||
const html = renderToStaticMarkup(<PaperclipLoading className="min-h-0" />);
|
||||
|
||||
expect(html).toContain("min-h-0");
|
||||
expect(html).not.toContain("min-h-dvh");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,36 @@
|
||||
import type { SVGProps } from "react";
|
||||
import { cn } from "../lib/utils";
|
||||
|
||||
export function AnimatedPaperclipIcon({ className, ...props }: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
viewBox="-1 -1 26 26"
|
||||
className={cn("paperclip-thinking-icon", className)}
|
||||
aria-hidden="true"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
className="paperclip-thinking-icon-path"
|
||||
d="M16 6 l-8.414 8.586 a2.000 2.000 0 0 0 2.828 2.828 l8.414 -8.586 a4.000 4.000 0 1 0 -5.657 -5.657 l-8.379 8.551 a6.000 6.000 0 1 0 8.485 8.485 l8.379 -8.551"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
/** Full-page loading state: a large, centered, gray animated paperclip. */
|
||||
export function PaperclipLoading({ className }: { className?: string }) {
|
||||
return (
|
||||
<div
|
||||
role="status"
|
||||
className={cn("flex min-h-dvh w-full items-center justify-center", className)}
|
||||
>
|
||||
<AnimatedPaperclipIcon className="h-24 w-24 text-muted-foreground" />
|
||||
<span className="sr-only">Loading…</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import { authApi } from "@/api/auth";
|
||||
import { healthApi } from "@/api/health";
|
||||
import { queryKeys } from "@/lib/queryKeys";
|
||||
import { BootstrapPendingPage } from "@/components/BootstrapPendingPage";
|
||||
import { PaperclipLoading } from "@/components/AnimatedPaperclipIcon";
|
||||
import { Card } from "@/components/ui/card";
|
||||
|
||||
function NoBoardAccessPage() {
|
||||
@@ -74,7 +75,7 @@ export function CloudAccessGate() {
|
||||
(isAuthenticatedMode && sessionQuery.isLoading) ||
|
||||
(isAuthenticatedMode && !isBootstrapPending && !!sessionQuery.data && boardAccessQuery.isLoading)
|
||||
) {
|
||||
return <div className="mx-auto max-w-xl py-10 text-sm text-muted-foreground">Loading...</div>;
|
||||
return <PaperclipLoading />;
|
||||
}
|
||||
|
||||
if (healthQuery.error || boardAccessQuery.error) {
|
||||
@@ -92,7 +93,7 @@ export function CloudAccessGate() {
|
||||
if (isBootstrapPending) {
|
||||
const health = healthQuery.data;
|
||||
if (!health) {
|
||||
return <div className="mx-auto max-w-xl py-10 text-sm text-muted-foreground">Loading...</div>;
|
||||
return <PaperclipLoading />;
|
||||
}
|
||||
const claimError = claimMutation.error instanceof ApiError
|
||||
? { status: claimMutation.error.status, message: claimMutation.error.message }
|
||||
|
||||
Reference in new issue
Block a user