Replace loading icons with custom Alexandria logo component
- Create LoadingSpinner component with Alexandria logo and animated text
- Replace all loading states with branded loading experience
- Add three size variants (small, medium, large) for different contexts
- Implement animated dots with accessibility support (prefers-reduced-motion)
- Add responsive design and high contrast mode support
- Update Home and Reader pages to use new loading component
- Remove old generic loading styles from CSS files
- Ensure consistent visual identity during all loading states
author
Pablo Murad
date
07 Jan 2026, 02:15 -0300
commit
1de2fec10e5276b78d04244ff264a535e2ffc659
parent
2cbbd29213dcfebc7a3c7dbf66d580e25e7d6a85
changes
8 files +377 −49
@@ -0,0 +1,156 @@ +# Componente de Loading Personalizado - Alexandria
+
+## Visão Geral
+
+O Alexandria agora usa um componente de loading personalizado que exibe a logo do projeto com uma animação de texto "Loading" embaixo, mantendo a consistência visual com o tema Y2K.
+
+## Componente LoadingSpinner
+
+### Localização
+- `src/components/LoadingSpinner/LoadingSpinner.tsx`
+- `src/components/LoadingSpinner/LoadingSpinner.css`
+
+### Características
+
+#### Visual
+- **Logo estática**: Exibe a logo do Alexandria (alexandria.png)
+- **Texto animado**: "Loading" com pontos animados (...)
+- **Centralizado**: Posicionado no centro da página
+- **Tema Y2K**: Cores e estilos consistentes com o design geral
+
+#### Funcionalidades
+- **Responsivo**: Adapta-se a diferentes tamanhos de tela
+- **Acessível**: Suporte a `prefers-reduced-motion` e `prefers-contrast`
+- **Flexível**: Diferentes tamanhos (small, medium, large)
+- **Mensagem customizável**: Texto pode ser personalizado
+
+### Propriedades
+
+```typescript
+interface LoadingSpinnerProps {
+ message?: string // Padrão: "Loading"
+ size?: 'small' | 'medium' | 'large' // Padrão: "medium"
+ className?: string // Classes CSS adicionais
+}
+```
+
+### Tamanhos
+
+#### Small
+- Logo: 80px (60px em mobile)
+- Altura mínima: 120px
+- Uso: Loading de componentes pequenos (ToC)
+
+#### Medium
+- Logo: 120px (100px em mobile)
+- Altura mínima: 200px
+- Uso: Loading de conteúdo (Reader)
+
+#### Large
+- Logo: 160px (120px em mobile)
+- Altura mínima: 300px
+- Uso: Loading inicial (Home)
+
+## Implementação
+
+### Home Page
+```tsx
+<LoadingSpinner
+ message="Loading Alexandria"
+ size="large"
+/>
+```
+
+### Reader Page
+```tsx
+<LoadingSpinner
+ message="Loading content"
+ size="medium"
+/>
+```
+
+### ToC Loading
+```tsx
+<LoadingSpinner
+ message="Loading navigation"
+ size="small"
+/>
+```
+
+## Animação
+
+### Pontos Animados
+- **Duração**: 1.4s por ciclo
+- **Efeito**: Fade in/out + scale
+- **Sequência**: Os três pontos animam em sequência
+- **Acessibilidade**: Desabilitado com `prefers-reduced-motion`
+
+### CSS Keyframes
+```css
+@keyframes loading-dot {
+ 0%, 80%, 100% {
+ opacity: 0.3;
+ transform: scale(0.8);
+ }
+ 40% {
+ opacity: 1;
+ transform: scale(1);
+ }
+}
+```
+
+## Detecção de Ambiente
+
+O componente detecta automaticamente o ambiente para carregar a logo correta:
+
+```typescript
+const getLogoPath = () => {
+ // Produção (GitHub Pages)
+ if (window.location.hostname.includes('github.io')) {
+ return '/alexandria/alexandria.png'
+ } else {
+ // Desenvolvimento
+ return '/alexandria.png'
+ }
+}
+```
+
+## Acessibilidade
+
+### Reduced Motion
+- Usuários com `prefers-reduced-motion: reduce` veem pontos estáticos
+- Mantém funcionalidade sem animação
+
+### High Contrast
+- Bordas mais espessas na logo
+- Texto em negrito
+- Cores de alto contraste
+
+### Responsive Design
+- Tamanhos adaptáveis para mobile
+- Padding e espaçamento otimizados
+- Logo redimensionada automaticamente
+
+## Vantagens
+
+### Antes
+- ❌ Ícone genérico de loading (🔄)
+- ❌ Texto simples sem identidade visual
+- ❌ Inconsistente entre páginas
+
+### Depois
+- ✅ Logo do Alexandria como elemento central
+- ✅ Animação suave e profissional
+- ✅ Consistência visual em todo o app
+- ✅ Diferentes tamanhos para diferentes contextos
+- ✅ Totalmente acessível e responsivo
+
+## Integração
+
+O componente substitui todos os antigos elementos de loading:
+- Home page: Loading inicial do README
+- Reader page: Loading de conteúdo
+- ToC: Loading de navegação
+- Botões: Estados de loading em ações
+
+Isso cria uma experiência de usuário mais coesa e profissional, mantendo a identidade visual do Alexandria em todos os momentos de carregamento.
@@ -0,0 +1,157 @@ +/* Loading Spinner - Y2K Style with Alexandria Logo */
+.loading-spinner {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ text-align: center;
+ padding: 32px 16px;
+ min-height: 200px;
+}
+
+/* Logo container */
+.loading-logo-container {
+ margin-bottom: 16px;
+}
+
+.loading-logo {
+ display: block;
+ max-width: 120px;
+ height: auto;
+ border: 1px solid var(--border-color);
+ background-color: var(--background-color);
+ opacity: 0.9;
+}
+
+/* Loading text with animated dots */
+.loading-text {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-family: Arial, sans-serif;
+ font-size: 14px;
+ color: var(--text-secondary);
+ font-weight: normal;
+}
+
+.loading-message {
+ margin-right: 4px;
+}
+
+.loading-dots {
+ display: inline-flex;
+ align-items: center;
+}
+
+.loading-dots .dot {
+ animation: loading-dot 1.4s infinite ease-in-out both;
+ font-weight: bold;
+ font-size: 16px;
+}
+
+.loading-dots .dot:nth-child(1) {
+ animation-delay: -0.32s;
+}
+
+.loading-dots .dot:nth-child(2) {
+ animation-delay: -0.16s;
+}
+
+.loading-dots .dot:nth-child(3) {
+ animation-delay: 0s;
+}
+
+@keyframes loading-dot {
+ 0%, 80%, 100% {
+ opacity: 0.3;
+ transform: scale(0.8);
+ }
+ 40% {
+ opacity: 1;
+ transform: scale(1);
+ }
+}
+
+/* Size variations */
+.loading-spinner.small {
+ min-height: 120px;
+ padding: 16px 8px;
+}
+
+.loading-spinner.small .loading-logo {
+ max-width: 80px;
+}
+
+.loading-spinner.small .loading-text {
+ font-size: 12px;
+}
+
+.loading-spinner.medium {
+ min-height: 200px;
+ padding: 32px 16px;
+}
+
+.loading-spinner.medium .loading-logo {
+ max-width: 120px;
+}
+
+.loading-spinner.medium .loading-text {
+ font-size: 14px;
+}
+
+.loading-spinner.large {
+ min-height: 300px;
+ padding: 48px 24px;
+}
+
+.loading-spinner.large .loading-logo {
+ max-width: 160px;
+}
+
+.loading-spinner.large .loading-text {
+ font-size: 16px;
+}
+
+/* Responsive adjustments */
+@media (max-width: 600px) {
+ .loading-spinner {
+ padding: 24px 12px;
+ min-height: 160px;
+ }
+
+ .loading-spinner.small .loading-logo {
+ max-width: 60px;
+ }
+
+ .loading-spinner.medium .loading-logo {
+ max-width: 100px;
+ }
+
+ .loading-spinner.large .loading-logo {
+ max-width: 120px;
+ }
+
+ .loading-text {
+ font-size: 12px;
+ }
+}
+
+/* Accessibility */
+@media (prefers-reduced-motion: reduce) {
+ .loading-dots .dot {
+ animation: none;
+ opacity: 0.7;
+ }
+}
+
+/* High contrast mode */
+@media (prefers-contrast: high) {
+ .loading-logo {
+ border: 2px solid var(--text-color);
+ }
+
+ .loading-text {
+ color: var(--text-color);
+ font-weight: bold;
+ }
+}
@@ -0,0 +1,47 @@ +import React from 'react'
+import './LoadingSpinner.css'
+
+interface LoadingSpinnerProps {
+ message?: string
+ size?: 'small' | 'medium' | 'large'
+ className?: string
+}
+
+const LoadingSpinner: React.FC<LoadingSpinnerProps> = ({
+ message = 'Loading',
+ size = 'medium',
+ className = ''
+}) => {
+ // Determine logo path based on environment
+ const getLogoPath = () => {
+ // In production (GitHub Pages)
+ if (window.location.hostname.includes('github.io')) {
+ return '/alexandria/alexandria.png'
+ } else {
+ // Development
+ return '/alexandria.png'
+ }
+ }
+
+ return (
+ <div className={`loading-spinner ${size} ${className}`}>
+ <div className="loading-logo-container">
+ <img
+ src={getLogoPath()}
+ alt="Alexandria Logo"
+ className="loading-logo"
+ />
+ </div>
+ <div className="loading-text">
+ <span className="loading-message">{message}</span>
+ <span className="loading-dots">
+ <span className="dot">.</span>
+ <span className="dot">.</span>
+ <span className="dot">.</span>
+ </span>
+ </div>
+ </div>
+ )
+}
+
+export default LoadingSpinner
@@ -57,20 +57,6 @@ white-space: nowrap; border: 0;
}
-.loading {
- display: inline-block;
- width: 16px;
- height: 16px;
- border: 2px solid var(--border-color);
- border-radius: 50%;
- border-top-color: var(--primary-color);
- animation: spin 1s linear infinite;
-}
-
-@keyframes spin {
- to { transform: rotate(360deg); }
-}
-
/* Enhanced Unicode font support for specific elements */
h1, h2, h3, h4, h5, h6 {
font-family:
@@ -5,17 +5,6 @@ padding: 16px 8px; background-color: var(--background-color);
}
-.loading {
- text-align: center;
- padding: 32px 16px;
-}
-
-.loading h2 {
- color: var(--text-secondary);
- font-weight: normal;
- font-size: 14px;
-}
-
.readme-content {
margin-bottom: 24px;
line-height: 1.4;
@@ -4,6 +4,7 @@ import { createServices } from '../../services' import { configurationManager } from '../../services'
import { RandomEngine } from '../../services/RandomEngine'
import MarkdownRenderer from '../../components/MarkdownRenderer/MarkdownRenderer'
+import LoadingSpinner from '../../components/LoadingSpinner/LoadingSpinner'
// ConfigurationBanner import removed - component kept for potential future use
// import ConfigurationBanner from '../../components/ConfigurationBanner/ConfigurationBanner'
import ErrorDisplay from '../../components/ErrorDisplay/ErrorDisplay'
@@ -199,9 +200,10 @@ if (isLoadingReadme) {
return (
<div className="home">
- <div className="loading">
- <h2>🔄 Loading Alexandria...</h2>
- </div>
+ <LoadingSpinner
+ message="Loading Alexandria"
+ size="large"
+ />
</div>
)
}
@@ -5,17 +5,6 @@ padding: 16px 8px; background-color: var(--background-color);
}
-.loading {
- text-align: center;
- padding: 32px 16px;
-}
-
-.loading h2 {
- color: var(--text-secondary);
- font-weight: normal;
- font-size: 14px;
-}
-
.error {
text-align: center;
padding: 32px 16px;
@@ -126,13 +115,10 @@ font-size: 12px; }
.toc-loading {
- background: #f8f9fa;
- border: 1px solid #e9ecef;
- border-radius: 8px;
- padding: 1rem;
+ background: var(--background-color);
+ border: 1px solid var(--border-color);
+ padding: 8px;
text-align: center;
- color: var(--text-secondary);
- font-size: 11px;
}
.error-message {
@@ -3,6 +3,7 @@ import { useParams, useNavigate } from 'react-router-dom' import { createServices } from '../../services'
import { RandomEngine } from '../../services/RandomEngine'
import MarkdownRenderer from '../../components/MarkdownRenderer/MarkdownRenderer'
+import LoadingSpinner from '../../components/LoadingSpinner/LoadingSpinner'
import DocumentToc from '../../components/DocumentToc/DocumentToc'
import RepositoryToc from '../../components/RepositoryToc/RepositoryToc'
import NavigationHistory from '../../components/NavigationHistory/NavigationHistory'
@@ -199,9 +200,10 @@ if (isLoading) {
return (
<div className="reader">
- <div className="loading">
- <h2>🔄 Loading content...</h2>
- </div>
+ <LoadingSpinner
+ message="Loading content"
+ size="medium"
+ />
</div>
)
}
@@ -306,7 +308,10 @@ )}
{isLoadingToc && (
<div className="toc-loading">
- <p>🔄 Loading navigation...</p>
+ <LoadingSpinner
+ message="Loading navigation"
+ size="small"
+ />
</div>
)}
</div>