code.pablomurad.com

alexandria

An endless, read-only library of Markdown from public GitHub repositories

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
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>