mirror of
https://github.com/dergigi/boris.git
synced 2025-12-19 07:34:28 +01:00
feat: add skeleton components and theme provider
This commit is contained in:
@@ -13,6 +13,7 @@ import Toast from './components/Toast'
|
|||||||
import { useToast } from './hooks/useToast'
|
import { useToast } from './hooks/useToast'
|
||||||
import { useOnlineStatus } from './hooks/useOnlineStatus'
|
import { useOnlineStatus } from './hooks/useOnlineStatus'
|
||||||
import { RELAYS } from './config/relays'
|
import { RELAYS } from './config/relays'
|
||||||
|
import { SkeletonThemeProvider } from './components/Skeletons'
|
||||||
|
|
||||||
const DEFAULT_ARTICLE = import.meta.env.VITE_DEFAULT_ARTICLE_NADDR ||
|
const DEFAULT_ARTICLE = import.meta.env.VITE_DEFAULT_ARTICLE_NADDR ||
|
||||||
'naddr1qvzqqqr4gupzqmjxss3dld622uu8q25gywum9qtg4w4cv4064jmg20xsac2aam5nqqxnzd3cxqmrzv3exgmr2wfesgsmew'
|
'naddr1qvzqqqr4gupzqmjxss3dld622uu8q25gywum9qtg4w4cv4064jmg20xsac2aam5nqqxnzd3cxqmrzv3exgmr2wfesgsmew'
|
||||||
@@ -271,6 +272,7 @@ function App() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<SkeletonThemeProvider>
|
||||||
<EventStoreProvider eventStore={eventStore}>
|
<EventStoreProvider eventStore={eventStore}>
|
||||||
<AccountsProvider manager={accountManager}>
|
<AccountsProvider manager={accountManager}>
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
@@ -287,6 +289,7 @@ function App() {
|
|||||||
)}
|
)}
|
||||||
</AccountsProvider>
|
</AccountsProvider>
|
||||||
</EventStoreProvider>
|
</EventStoreProvider>
|
||||||
|
</SkeletonThemeProvider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
42
src/components/Skeletons/BlogPostSkeleton.tsx
Normal file
42
src/components/Skeletons/BlogPostSkeleton.tsx
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import Skeleton from 'react-loading-skeleton'
|
||||||
|
|
||||||
|
export const BlogPostSkeleton: React.FC = () => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="blog-post-card"
|
||||||
|
style={{
|
||||||
|
textDecoration: 'none',
|
||||||
|
color: 'inherit',
|
||||||
|
display: 'block'
|
||||||
|
}}
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<div className="blog-post-card-image">
|
||||||
|
<Skeleton height={200} style={{ display: 'block' }} />
|
||||||
|
</div>
|
||||||
|
<div className="blog-post-card-content">
|
||||||
|
<Skeleton
|
||||||
|
height={24}
|
||||||
|
width="85%"
|
||||||
|
style={{ marginBottom: '0.75rem' }}
|
||||||
|
className="blog-post-card-title"
|
||||||
|
/>
|
||||||
|
<Skeleton
|
||||||
|
count={2}
|
||||||
|
style={{ marginBottom: '0.5rem' }}
|
||||||
|
className="blog-post-card-summary"
|
||||||
|
/>
|
||||||
|
<div className="blog-post-card-meta" style={{ display: 'flex', gap: '1rem' }}>
|
||||||
|
<span className="blog-post-card-author" style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
|
||||||
|
<Skeleton width={100} height={14} />
|
||||||
|
</span>
|
||||||
|
<span className="blog-post-card-date" style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
|
||||||
|
<Skeleton width={80} height={14} />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
80
src/components/Skeletons/BookmarkSkeleton.tsx
Normal file
80
src/components/Skeletons/BookmarkSkeleton.tsx
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import Skeleton from 'react-loading-skeleton'
|
||||||
|
import { ViewMode } from '../Bookmarks'
|
||||||
|
|
||||||
|
interface BookmarkSkeletonProps {
|
||||||
|
viewMode: ViewMode
|
||||||
|
}
|
||||||
|
|
||||||
|
export const BookmarkSkeleton: React.FC<BookmarkSkeletonProps> = ({ viewMode }) => {
|
||||||
|
if (viewMode === 'compact') {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="bookmark-item-compact"
|
||||||
|
style={{ padding: '0.75rem', marginBottom: '0.5rem' }}
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<div style={{ display: 'flex', gap: '0.75rem', alignItems: 'flex-start' }}>
|
||||||
|
<Skeleton width={40} height={40} />
|
||||||
|
<div style={{ flex: 1, minWidth: 0 }}>
|
||||||
|
<Skeleton width="80%" height={16} style={{ marginBottom: '0.25rem' }} />
|
||||||
|
<Skeleton width="60%" height={14} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (viewMode === 'cards') {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="bookmark-card"
|
||||||
|
style={{
|
||||||
|
borderRadius: '8px',
|
||||||
|
overflow: 'hidden',
|
||||||
|
backgroundColor: 'var(--color-bg-elevated)',
|
||||||
|
marginBottom: '1rem'
|
||||||
|
}}
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<Skeleton height={160} style={{ display: 'block' }} />
|
||||||
|
<div style={{ padding: '1rem' }}>
|
||||||
|
<Skeleton height={20} width="90%" style={{ marginBottom: '0.5rem' }} />
|
||||||
|
<Skeleton count={2} style={{ marginBottom: '0.5rem' }} />
|
||||||
|
<div style={{ display: 'flex', gap: '0.5rem', marginTop: '0.75rem' }}>
|
||||||
|
<Skeleton width={80} height={14} />
|
||||||
|
<Skeleton width={60} height={14} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// large view
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="bookmark-large"
|
||||||
|
style={{
|
||||||
|
marginBottom: '1.5rem',
|
||||||
|
borderRadius: '8px',
|
||||||
|
overflow: 'hidden',
|
||||||
|
backgroundColor: 'var(--color-bg-elevated)'
|
||||||
|
}}
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<Skeleton height={240} style={{ display: 'block' }} />
|
||||||
|
<div style={{ padding: '1.5rem' }}>
|
||||||
|
<Skeleton height={24} width="85%" style={{ marginBottom: '0.75rem' }} />
|
||||||
|
<Skeleton count={3} style={{ marginBottom: '0.5rem' }} />
|
||||||
|
<div style={{ display: 'flex', gap: '1rem', marginTop: '1rem' }}>
|
||||||
|
<Skeleton circle width={32} height={32} />
|
||||||
|
<div style={{ flex: 1 }}>
|
||||||
|
<Skeleton width={120} height={14} style={{ marginBottom: '0.25rem' }} />
|
||||||
|
<Skeleton width={100} height={12} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
66
src/components/Skeletons/ContentSkeleton.tsx
Normal file
66
src/components/Skeletons/ContentSkeleton.tsx
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import Skeleton from 'react-loading-skeleton'
|
||||||
|
|
||||||
|
export const ContentSkeleton: React.FC = () => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="reader-content"
|
||||||
|
style={{
|
||||||
|
maxWidth: '900px',
|
||||||
|
margin: '0 auto',
|
||||||
|
padding: '2rem 1rem'
|
||||||
|
}}
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
{/* Title */}
|
||||||
|
<Skeleton
|
||||||
|
height={48}
|
||||||
|
width="90%"
|
||||||
|
style={{ marginBottom: '1rem' }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Byline / Meta */}
|
||||||
|
<div style={{ display: 'flex', gap: '1rem', marginBottom: '2rem', alignItems: 'center' }}>
|
||||||
|
<Skeleton circle width={40} height={40} />
|
||||||
|
<div style={{ flex: 1 }}>
|
||||||
|
<Skeleton width={150} height={16} style={{ marginBottom: '0.25rem' }} />
|
||||||
|
<Skeleton width={200} height={14} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Cover image */}
|
||||||
|
<Skeleton
|
||||||
|
height={400}
|
||||||
|
style={{ marginBottom: '2rem', display: 'block', borderRadius: '8px' }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Paragraphs */}
|
||||||
|
<div style={{ marginBottom: '1.5rem' }}>
|
||||||
|
<Skeleton count={3} style={{ marginBottom: '0.5rem' }} />
|
||||||
|
<Skeleton width="80%" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ marginBottom: '1.5rem' }}>
|
||||||
|
<Skeleton count={4} style={{ marginBottom: '0.5rem' }} />
|
||||||
|
<Skeleton width="65%" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ marginBottom: '1.5rem' }}>
|
||||||
|
<Skeleton count={3} style={{ marginBottom: '0.5rem' }} />
|
||||||
|
<Skeleton width="90%" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Another image placeholder */}
|
||||||
|
<Skeleton
|
||||||
|
height={300}
|
||||||
|
style={{ marginBottom: '2rem', display: 'block', borderRadius: '8px' }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div style={{ marginBottom: '1.5rem' }}>
|
||||||
|
<Skeleton count={3} style={{ marginBottom: '0.5rem' }} />
|
||||||
|
<Skeleton width="75%" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
36
src/components/Skeletons/HighlightSkeleton.tsx
Normal file
36
src/components/Skeletons/HighlightSkeleton.tsx
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import Skeleton from 'react-loading-skeleton'
|
||||||
|
|
||||||
|
export const HighlightSkeleton: React.FC = () => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="highlight-item"
|
||||||
|
style={{
|
||||||
|
padding: '1rem',
|
||||||
|
marginBottom: '0.75rem',
|
||||||
|
borderRadius: '8px',
|
||||||
|
backgroundColor: 'var(--color-bg-elevated)'
|
||||||
|
}}
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
{/* Author line with avatar */}
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', marginBottom: '0.75rem' }}>
|
||||||
|
<Skeleton circle width={24} height={24} />
|
||||||
|
<Skeleton width={120} height={14} />
|
||||||
|
<Skeleton width={60} height={12} style={{ marginLeft: 'auto' }} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Highlight content */}
|
||||||
|
<div style={{ marginBottom: '0.5rem' }}>
|
||||||
|
<Skeleton count={2} style={{ marginBottom: '0.25rem' }} />
|
||||||
|
<Skeleton width="70%" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Citation/context */}
|
||||||
|
<div style={{ marginTop: '0.75rem' }}>
|
||||||
|
<Skeleton width="90%" height={12} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
49
src/components/Skeletons/SkeletonThemeProvider.tsx
Normal file
49
src/components/Skeletons/SkeletonThemeProvider.tsx
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import React, { useEffect, useState } from 'react'
|
||||||
|
import { SkeletonTheme } from 'react-loading-skeleton'
|
||||||
|
|
||||||
|
interface SkeletonThemeProviderProps {
|
||||||
|
children: React.ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SkeletonThemeProvider: React.FC<SkeletonThemeProviderProps> = ({ children }) => {
|
||||||
|
const [colors, setColors] = useState({
|
||||||
|
baseColor: '#27272a',
|
||||||
|
highlightColor: '#52525b'
|
||||||
|
})
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const updateColors = () => {
|
||||||
|
const rootStyles = getComputedStyle(document.documentElement)
|
||||||
|
const baseColor = rootStyles.getPropertyValue('--color-bg-elevated').trim() || '#27272a'
|
||||||
|
const highlightColor = rootStyles.getPropertyValue('--color-border-subtle').trim() || '#52525b'
|
||||||
|
|
||||||
|
setColors({ baseColor, highlightColor })
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initial update
|
||||||
|
updateColors()
|
||||||
|
|
||||||
|
// Watch for theme changes via MutationObserver
|
||||||
|
const observer = new MutationObserver((mutations) => {
|
||||||
|
mutations.forEach((mutation) => {
|
||||||
|
if (mutation.type === 'attributes' && mutation.attributeName === 'class') {
|
||||||
|
updateColors()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
observer.observe(document.documentElement, {
|
||||||
|
attributes: true,
|
||||||
|
attributeFilter: ['class']
|
||||||
|
})
|
||||||
|
|
||||||
|
return () => observer.disconnect()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SkeletonTheme baseColor={colors.baseColor} highlightColor={colors.highlightColor}>
|
||||||
|
{children}
|
||||||
|
</SkeletonTheme>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
6
src/components/Skeletons/index.ts
Normal file
6
src/components/Skeletons/index.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export { SkeletonThemeProvider } from './SkeletonThemeProvider'
|
||||||
|
export { BookmarkSkeleton } from './BookmarkSkeleton'
|
||||||
|
export { BlogPostSkeleton } from './BlogPostSkeleton'
|
||||||
|
export { HighlightSkeleton } from './HighlightSkeleton'
|
||||||
|
export { ContentSkeleton } from './ContentSkeleton'
|
||||||
|
|
||||||
@@ -13,6 +13,7 @@
|
|||||||
@import './styles/components/settings.css';
|
@import './styles/components/settings.css';
|
||||||
@import './styles/components/me.css';
|
@import './styles/components/me.css';
|
||||||
@import './styles/components/pull-to-refresh.css';
|
@import './styles/components/pull-to-refresh.css';
|
||||||
|
@import './styles/components/skeletons.css';
|
||||||
@import './styles/utils/animations.css';
|
@import './styles/utils/animations.css';
|
||||||
@import './styles/utils/utilities.css';
|
@import './styles/utils/utilities.css';
|
||||||
@import './styles/utils/legacy.css';
|
@import './styles/utils/legacy.css';
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import ReactDOM from 'react-dom/client'
|
|||||||
import App from './App.tsx'
|
import App from './App.tsx'
|
||||||
import './styles/tailwind.css'
|
import './styles/tailwind.css'
|
||||||
import './index.css'
|
import './index.css'
|
||||||
|
import 'react-loading-skeleton/dist/skeleton.css'
|
||||||
|
|
||||||
// Register Service Worker for PWA functionality
|
// Register Service Worker for PWA functionality
|
||||||
if ('serviceWorker' in navigator) {
|
if ('serviceWorker' in navigator) {
|
||||||
|
|||||||
35
src/styles/components/skeletons.css
Normal file
35
src/styles/components/skeletons.css
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
/* Skeleton loading animations - respects prefers-reduced-motion */
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.react-loading-skeleton {
|
||||||
|
animation: none !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ensure skeletons have proper border radius to match design */
|
||||||
|
.react-loading-skeleton {
|
||||||
|
border-radius: 4px;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Image skeleton aspect ratio boxes to prevent CLS */
|
||||||
|
.blog-post-card-image .react-loading-skeleton,
|
||||||
|
.bookmark-card .react-loading-skeleton:first-child {
|
||||||
|
aspect-ratio: 16 / 9;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Skeleton spacing adjustments */
|
||||||
|
.highlights-list .react-loading-skeleton,
|
||||||
|
.bookmarks-list .react-loading-skeleton {
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ensure skeletons inherit theme colors properly */
|
||||||
|
.react-loading-skeleton::after {
|
||||||
|
background: linear-gradient(
|
||||||
|
90deg,
|
||||||
|
transparent,
|
||||||
|
var(--color-border-subtle, rgba(255, 255, 255, 0.05)),
|
||||||
|
transparent
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Reference in New Issue
Block a user