feat(modal): large centered loading spinner
CI / lint (push) Successful in 2s
CI / frontend-build (push) Successful in 19s
CI / backend-lint-and-test (push) Successful in 25s
CI / integration (push) Successful in 3m3s

The old size-36 v-progress-circular sat tiny in the top-left because
.fc-viewer__media doesn't center its children (the canvas centers itself).
Replace it with a 108px dual counter-rotating accent-ring spinner as a centered,
non-interactive overlay over the modal (operator-flagged 2026-06-07).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-07 10:42:05 -04:00
parent 68cda6114d
commit 6c5dbfe4a0
+37 -4
View File
@@ -47,12 +47,17 @@
<v-icon size="32">mdi-chevron-right</v-icon>
</button>
<!-- Large centered loading spinner (operator-flagged 2026-06-07: the old
size-36 one sat tiny in the top-left). Dual counter-rotating accent
rings, centered over the whole modal. pointer-events:none so the
close/nav controls underneath stay clickable. -->
<div v-if="modal.loading" class="fc-viewer__loading">
<div class="fc-viewer__spinner" />
</div>
<div class="fc-viewer__body">
<div class="fc-viewer__media">
<v-progress-circular
v-if="modal.loading" indeterminate color="accent" size="36"
/>
<template v-else-if="modal.current">
<template v-if="modal.current">
<ImageCanvas
v-if="!isVideo" :src="modal.current.image_url" :alt="`Image ${modal.current.id}`"
@close-request="$emit('close')"
@@ -259,6 +264,34 @@ function nextFrame() {
right: calc(var(--fc-side-w) + 16px);
transform: translateY(-50%);
}
/* Centered loading overlay — sits over the media band; non-interactive so the
close/nav controls underneath keep working. */
.fc-viewer__loading {
position: absolute; inset: 0; z-index: 2;
display: flex; align-items: center; justify-content: center;
pointer-events: none;
}
/* Two concentric accent rings counter-rotating, each lit on a different arc —
reads as one orbiting object. ~108px so it's unmistakably the focal point. */
.fc-viewer__spinner {
width: 108px; height: 108px; border-radius: 50%;
border: 5px solid rgb(var(--v-theme-accent), 0.16);
border-top-color: rgb(var(--v-theme-accent));
animation: fc-viewer-spin 0.95s cubic-bezier(0.5, 0.1, 0.5, 0.9) infinite;
position: relative;
box-shadow: 0 0 24px rgb(var(--v-theme-accent), 0.18);
}
.fc-viewer__spinner::after {
content: ''; position: absolute; inset: 14px; border-radius: 50%;
border: 5px solid rgb(var(--v-theme-accent), 0.10);
border-bottom-color: rgb(var(--v-theme-accent));
animation: fc-viewer-spin 1.5s cubic-bezier(0.5, 0.1, 0.5, 0.9) infinite reverse;
}
@keyframes fc-viewer-spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) {
.fc-viewer__spinner, .fc-viewer__spinner::after { animation-duration: 3s; }
}
.fc-viewer__body {
flex: 1; display: flex; min-height: 0;
}