From c5ce68e05ca588a39047d978c6e11fff8b13d0b1 Mon Sep 17 00:00:00 2001 From: HAAHIT Date: Mon, 3 Aug 2026 22:10:48 +0530 Subject: [PATCH] feat(ui): adopt the designed loading treatment Ports the loading state from the "BoloDB App" design (Claude Design project 777adb72) to LoadingScreen, replacing the card-with-orbit-rings treatment. The design's loader is the BoloDB mark drawing itself: four shapes sharing one stroke-dash animation on a 0.15s stagger, so the bubble outline, the disc and the two bands appear in the order you would draw them by hand. Under it sits a label and a 120x1px progress line. No card, no blobs, no spinner -- the mark carries the motion, and the radial brand glow from the design sits behind it. Kept deliberately compatible: - message/submessage/variant are unchanged, so all five call sites work as they are. The design uses one treatment for every loading context, so variant no longer swaps the mark; it stays in the signature rather than breaking callers, and submessage renders under the label. - New optional `progress` (0-100) drives the determinate bar the design specifies. Left unset the bar sweeps, because none of the current call sites have real progress to report and inventing a percentage that jumps to 90% and waits is worse than an honest indeterminate bar. Added a prefers-reduced-motion path the previous component did not have. The mark is the only thing signalling "still working", so it keeps a slow opacity pulse rather than freezing outright. Verified in the browser against dark and light themes: every token the design uses (--brand, --ink-2, --faint, --border, --glow-rgb) already exists in layout.css for all themes, so the mark picks up brand blue on dark and brand green on light with no new variables. Confirmed the SVG resolves correctly when fully drawn by freezing the animation. svelte-check reports 0 errors and the production build succeeds. Co-Authored-By: Claude Opus 5 --- .../lib/components/ui/LoadingScreen.svelte | 370 +++++++++--------- 1 file changed, 185 insertions(+), 185 deletions(-) diff --git a/frontend/src/lib/components/ui/LoadingScreen.svelte b/frontend/src/lib/components/ui/LoadingScreen.svelte index ec23944..ea307fc 100644 --- a/frontend/src/lib/components/ui/LoadingScreen.svelte +++ b/frontend/src/lib/components/ui/LoadingScreen.svelte @@ -1,61 +1,98 @@ -
- -
-
+
+ + -
- -
- {#if variant === 'connect'} - -
-
- - - - - {:else if variant === 'query'} - -
-
- - - - - +
+ {message} + {#if submessage} + {submessage} + {/if} +
+ {#if determinate} +
{:else} - -
- - - -
+
{/if}
- - -
-
-
- - -

{message}

- {#if submessage} -

{submessage}

- {/if}
@@ -63,175 +100,138 @@ .loading-root { position: absolute; inset: 0; + z-index: 30; display: flex; + flex-direction: column; align-items: center; justify-content: center; - z-index: 30; + gap: 30px; overflow: hidden; - /* Matches the app background */ - background: var(--bg); - animation: fadeIn 0.22s ease both; + background: + radial-gradient( + 1000px 600px at 50% -10%, + rgba(var(--glow-rgb), 0.1) 0%, + transparent 60% + ), + var(--bg); + animation: riseIn 0.4s both; } - @keyframes fadeIn { - from { opacity: 0; } - to { opacity: 1; } + @keyframes riseIn { + from { + opacity: 0; + transform: translateY(14px); + } + to { + opacity: 1; + transform: none; + } } - /* ---- ambient blobs ---- */ - .blob { - position: absolute; - border-radius: 50%; - filter: blur(72px); - pointer-events: none; - } - .blob-a { - width: 420px; - height: 420px; - background: radial-gradient(circle, rgba(var(--glow-rgb), 0.18) 0%, transparent 70%); - top: -100px; - right: -60px; - animation: driftA 7s ease-in-out infinite alternate; - } - .blob-b { - width: 320px; - height: 320px; - background: radial-gradient(circle, rgba(var(--glow-rgb), 0.10) 0%, transparent 70%); - bottom: -80px; - left: -60px; - animation: driftB 9s ease-in-out infinite alternate; - } - @keyframes driftA { - from { transform: translate(0, 0) scale(1); } - to { transform: translate(-30px, 30px) scale(1.1); } - } - @keyframes driftB { - from { transform: translate(0, 0) scale(1); } - to { transform: translate(20px, -20px) scale(1.08); } + .mark { + overflow: visible; } - - /* ---- card ---- */ - .loading-card { - display: flex; - flex-direction: column; - align-items: center; - gap: 18px; - padding: 40px 48px; - background: var(--card); - border: 1px solid var(--border); - border-radius: var(--radius-xl, 26px); - box-shadow: var(--shadow-lg); - max-width: 340px; - width: 90%; - animation: cardPop 0.4s cubic-bezier(0.34, 1.56, 0.64, 1) both; - position: relative; - z-index: 1; - } - @keyframes cardPop { - from { opacity: 0; transform: scale(0.88) translateY(16px); } - to { opacity: 1; transform: scale(1) translateY(0); } + .mark > * { + stroke-dasharray: 1; + animation: signDraw 2.6s cubic-bezier(0.65, 0, 0.35, 1) infinite; } - - /* ---- icon area ---- */ - .icon-wrap { - position: relative; - width: 64px; - height: 64px; - display: flex; - align-items: center; - justify-content: center; + .mark > *:nth-child(2) { + animation-delay: 0.15s; } - - .icon-svg { - color: var(--brand); - position: relative; - z-index: 2; - animation: iconPulse 2.4s ease-in-out infinite; + .mark > *:nth-child(3) { + animation-delay: 0.3s; } - @keyframes iconPulse { - 0%, 100% { opacity: 1; transform: scale(1); } - 50% { opacity: 0.75; transform: scale(0.92); } + .mark > *:nth-child(4) { + animation-delay: 0.45s; } - /* orbit rings */ - .orbit-ring { - position: absolute; - inset: 0; - border-radius: 50%; - border: 1.5px solid transparent; - border-top-color: var(--brand); - animation: orbit 1.5s linear infinite; - } - .ring-1 { - inset: 0; - opacity: 0.55; - } - .ring-2 { - inset: 8px; - animation-direction: reverse; - animation-duration: 2.2s; - border-top-color: var(--accent, var(--brand)); - opacity: 0.35; - } - @keyframes orbit { - to { transform: rotate(360deg); } + @keyframes signDraw { + 0%, + 100% { + stroke-dashoffset: 1; + } + 50% { + stroke-dashoffset: 0; + } } - /* ---- dots ---- */ - .dots { + .copy { display: flex; - gap: 8px; + flex-direction: column; align-items: center; + gap: 12px; + padding: 0 24px; } - .dot { - width: 10px; - height: 10px; - border-radius: 50%; - background: var(--brand); - animation: dotBounce 1.2s ease-in-out infinite; + + .msg { + font-size: 14px; + font-weight: 600; + color: var(--ink-2); + letter-spacing: -0.005em; + text-align: center; } - .dot:nth-child(1) { animation-delay: 0s; } - .dot:nth-child(2) { animation-delay: 0.18s; } - .dot:nth-child(3) { animation-delay: 0.36s; } - @keyframes dotBounce { - 0%, 80%, 100% { transform: scale(0.7); opacity: 0.45; } - 40% { transform: scale(1.1); opacity: 1; } + + .sub { + font-size: 12.5px; + color: var(--faint); + line-height: 1.5; + text-align: center; + max-width: 260px; } - /* ---- progress bar ---- */ - .progress-track { - width: 100%; - height: 3px; - background: var(--surface-3, var(--border)); + .track { + width: 120px; + height: 1px; + background: var(--border); border-radius: 99px; overflow: hidden; } - .progress-bar { + + .fill { height: 100%; - width: 40%; - background: linear-gradient(90deg, transparent, var(--brand), var(--accent, var(--brand)), transparent); + background: var(--brand); border-radius: 99px; - animation: progressSweep 1.6s ease-in-out infinite; + transition: width 0.5s cubic-bezier(0.65, 0, 0.35, 1); } - @keyframes progressSweep { - 0% { transform: translateX(-200%); } - 100% { transform: translateX(350%); } + + .fill.sweep { + width: 40%; + animation: sweep 1.6s ease-in-out infinite; } - /* ---- text ---- */ - .loading-msg { - margin: 0; - font-size: 15px; - font-weight: 700; - color: var(--ink); - letter-spacing: -0.015em; - text-align: center; + @keyframes sweep { + 0% { + transform: translateX(-120%); + } + 100% { + transform: translateX(320%); + } } - .loading-sub { - margin: -8px 0 0; - font-size: 12.5px; - color: var(--muted); - text-align: center; - line-height: 1.5; + + /* + The mark is the only thing conveying "still working", so it keeps a gentle + pulse rather than freezing completely when motion is reduced. + */ + @media (prefers-reduced-motion: reduce) { + .loading-root { + animation: none; + } + .mark > * { + stroke-dasharray: none; + animation: fade 2.4s ease-in-out infinite; + } + .fill.sweep { + width: 100%; + animation: fade 2.4s ease-in-out infinite; + } + @keyframes fade { + 0%, + 100% { + opacity: 0.35; + } + 50% { + opacity: 1; + } + } }