mirror of
https://github.com/dergigi/boris.git
synced 2026-02-16 12:34:41 +01:00
Compare commits
39 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d5076ff53e | ||
|
|
41e452be1e | ||
|
|
f267df8f60 | ||
|
|
7426c9d1fc | ||
|
|
93d0c1052b | ||
|
|
6537650757 | ||
|
|
a95f9b522b | ||
|
|
47d1335842 | ||
|
|
168095e133 | ||
|
|
5c7b413a8d | ||
|
|
bca6458e44 | ||
|
|
ebdfa3b5a3 | ||
|
|
17480dddbf | ||
|
|
2a422fbeb9 | ||
|
|
22961ee479 | ||
|
|
18db905974 | ||
|
|
689963c041 | ||
|
|
3f8869fd75 | ||
|
|
129aced1a2 | ||
|
|
69febf4356 | ||
|
|
65051c9c1f | ||
|
|
ba8229d464 | ||
|
|
9251b017d4 | ||
|
|
1ae76031f3 | ||
|
|
994d834a0b | ||
|
|
67a4e17055 | ||
|
|
1e82e3f240 | ||
|
|
f973c75ff5 | ||
|
|
28316a71c5 | ||
|
|
cfc12e2d78 | ||
|
|
7464a8b505 | ||
|
|
938d79663b | ||
|
|
cc0ad69275 | ||
|
|
810ff060f8 | ||
|
|
5e03ef70a6 | ||
|
|
f05fb29c7b | ||
|
|
e737b1f7f0 | ||
|
|
21a7be2f98 | ||
|
|
4c720aa049 |
@@ -1,98 +0,0 @@
|
||||
# Boris Color System
|
||||
|
||||
All colors now use Tailwind CSS color palette for consistency and maintainability.
|
||||
|
||||
## Semantic Color Aliases (Tailwind Config)
|
||||
|
||||
```javascript
|
||||
'app-bg': '#18181b', // zinc-900 - Main backgrounds
|
||||
'app-bg-elevated': '#27272a', // zinc-800 - Elevated surfaces (cards, modals)
|
||||
'app-bg-subtle': '#1e1e1e', // Custom ~zinc-850 - Subtle backgrounds
|
||||
'app-border': '#3f3f46', // zinc-700 - Primary borders
|
||||
'app-border-subtle': '#52525b', // zinc-600 - Subtle borders
|
||||
'app-text': '#e4e4e7', // zinc-200 - Primary text
|
||||
'app-text-secondary': '#a1a1aa', // zinc-400 - Secondary text
|
||||
'app-text-muted': '#71717a', // zinc-500 - Muted text
|
||||
'primary': '#6366f1', // indigo-500 - Primary accent
|
||||
'primary-hover': '#4f46e5', // indigo-600 - Primary hover state
|
||||
'highlight-mine': '#fde047', // yellow-300 - User highlights
|
||||
'highlight-friends': '#f97316', // orange-500 - Friends highlights
|
||||
'highlight-nostrverse': '#9333ea', // purple-600 - Nostrverse highlights
|
||||
```
|
||||
|
||||
## Highlight Colors (User-Settable)
|
||||
|
||||
Default colors in the color picker:
|
||||
- **Yellow** (default): `#fde047` - yellow-300
|
||||
- **Orange**: `#f97316` - orange-500
|
||||
- **Pink**: `#ec4899` - pink-500
|
||||
- **Green**: `#22c55e` - green-500
|
||||
- **Blue**: `#3b82f6` - blue-500
|
||||
- **Purple**: `#9333ea` - purple-600
|
||||
|
||||
## Common Color Mappings
|
||||
|
||||
| Old Hex | Tailwind Color | Usage |
|
||||
|-----------|----------------|-------|
|
||||
| `#18181b` | zinc-900 | Main app background |
|
||||
| `#1a1a1a` | zinc-900 | Component backgrounds |
|
||||
| `#1e1e1e` | ~zinc-850 | Code blocks, subtle surfaces |
|
||||
| `#252525` | zinc-800 | Hover states |
|
||||
| `#27272a` | zinc-800 | Elevated surfaces |
|
||||
| `#2a2a2a` | zinc-800 | Buttons, inputs |
|
||||
| `#333` | zinc-700 | Primary borders |
|
||||
| `#3f3f46` | zinc-700 | Component borders |
|
||||
| `#444` | zinc-600 | Subtle borders |
|
||||
| `#52525b` | zinc-600 | Input borders |
|
||||
| `#555` | zinc-500 | Hover borders |
|
||||
| `#666` | zinc-500 | Muted text |
|
||||
| `#71717a` | zinc-500 | Secondary text |
|
||||
| `#888` | zinc-400 | Secondary text |
|
||||
| `#999` | zinc-400 | Muted labels |
|
||||
| `#a1a1aa` | zinc-400 | Placeholder text |
|
||||
| `#aaa` | zinc-300 | Light text |
|
||||
| `#ccc` | zinc-300 | Primary text on dark |
|
||||
| `#ddd` | zinc-200 | Bright text |
|
||||
| `#e4e4e7` | zinc-200 | Primary text |
|
||||
| `#646cff` | indigo-500 | Primary accent |
|
||||
| `#535bf2` | indigo-600 | Primary hover |
|
||||
| `#fde047` | yellow-300 | Default highlight (brighter) |
|
||||
| `#f97316` | orange-500 | Friends highlights |
|
||||
| `#9333ea` | purple-600 | Nostrverse highlights |
|
||||
|
||||
## Usage Guidelines
|
||||
|
||||
### In CSS
|
||||
Use Tailwind utilities whenever possible:
|
||||
```css
|
||||
.example {
|
||||
background: rgb(24 24 27); /* zinc-900 */
|
||||
border: 1px solid rgb(63 63 70); /* zinc-700 */
|
||||
color: rgb(228 228 231); /* zinc-200 */
|
||||
}
|
||||
```
|
||||
|
||||
### In TSX
|
||||
Use Tailwind classes:
|
||||
```tsx
|
||||
<div className="bg-zinc-900 border border-zinc-700 text-zinc-200">
|
||||
```
|
||||
|
||||
Or semantic aliases:
|
||||
```tsx
|
||||
<div className="bg-app-bg border border-app-border text-app-text">
|
||||
```
|
||||
|
||||
### CSS Variables (User-Settable)
|
||||
For colors that users can customize:
|
||||
```css
|
||||
background: var(--highlight-color-mine, #fde047);
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
- All hex colors are now Tailwind palette colors
|
||||
- CSS variables remain for user-customizable colors
|
||||
- Semantic aliases provide easier maintenance
|
||||
- RGB format in CSS allows for opacity control
|
||||
|
||||
@@ -25,6 +25,11 @@
|
||||
<meta name="twitter:url" content="https://read.withboris.com/" />
|
||||
<meta name="twitter:title" content="Boris - Nostr Bookmarks" />
|
||||
<meta name="twitter:description" content="Your reading list for the Nostr world. A minimal nostr client for bookmark management with highlights." />
|
||||
|
||||
<!-- Default to system theme until settings load from Nostr -->
|
||||
<script>
|
||||
document.documentElement.className = 'theme-system';
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "boris",
|
||||
"version": "0.6.2",
|
||||
"version": "0.6.4",
|
||||
"description": "A minimal nostr client for bookmark management",
|
||||
"homepage": "https://read.withboris.com/",
|
||||
"type": "module",
|
||||
|
||||
18
src/App.tsx
18
src/App.tsx
@@ -110,6 +110,24 @@ function AppRoutes({
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/p/:npub"
|
||||
element={
|
||||
<Bookmarks
|
||||
relayPool={relayPool}
|
||||
onLogout={handleLogout}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/p/:npub/writings"
|
||||
element={
|
||||
<Bookmarks
|
||||
relayPool={relayPool}
|
||||
onLogout={handleLogout}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Route path="/" element={<Navigate to={`/a/${DEFAULT_ARTICLE}`} replace />} />
|
||||
</Routes>
|
||||
)
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
import React from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import { faUserCircle } from '@fortawesome/free-solid-svg-icons'
|
||||
import { useEventModel } from 'applesauce-react/hooks'
|
||||
import { Models } from 'applesauce-core'
|
||||
import { nip19 } from 'nostr-tools'
|
||||
|
||||
interface AuthorCardProps {
|
||||
authorPubkey: string
|
||||
clickable?: boolean
|
||||
}
|
||||
|
||||
const AuthorCard: React.FC<AuthorCardProps> = ({ authorPubkey }) => {
|
||||
const AuthorCard: React.FC<AuthorCardProps> = ({ authorPubkey, clickable = true }) => {
|
||||
const navigate = useNavigate()
|
||||
const profile = useEventModel(Models.ProfileModel, [authorPubkey])
|
||||
|
||||
const getAuthorName = () => {
|
||||
@@ -20,8 +24,19 @@ const AuthorCard: React.FC<AuthorCardProps> = ({ authorPubkey }) => {
|
||||
const authorImage = profile?.picture || profile?.image
|
||||
const authorBio = profile?.about
|
||||
|
||||
const handleClick = () => {
|
||||
if (clickable) {
|
||||
const npub = nip19.npubEncode(authorPubkey)
|
||||
navigate(`/p/${npub}`)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="author-card">
|
||||
<div
|
||||
className={`author-card ${clickable ? 'author-card-clickable' : ''}`}
|
||||
onClick={handleClick}
|
||||
style={clickable ? { cursor: 'pointer' } : undefined}
|
||||
>
|
||||
<div className="author-card-avatar">
|
||||
{authorImage ? (
|
||||
<img src={authorImage} alt={getAuthorName()} />
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useState } from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import { faBookmark, faUserLock, faChevronDown, faChevronUp, faGlobe } from '@fortawesome/free-solid-svg-icons'
|
||||
import { IndividualBookmark } from '../../types/bookmarks'
|
||||
@@ -10,7 +11,7 @@ import { IconGetter } from './shared'
|
||||
import { useImageCache } from '../../hooks/useImageCache'
|
||||
import { getPreviewImage, fetchOgImage } from '../../utils/imagePreview'
|
||||
import { UserSettings } from '../../services/settingsService'
|
||||
import { getProfileUrl, getEventUrl } from '../../config/nostrGateways'
|
||||
import { getEventUrl } from '../../config/nostrGateways'
|
||||
|
||||
interface CardViewProps {
|
||||
bookmark: IndividualBookmark
|
||||
@@ -190,16 +191,14 @@ export const CardView: React.FC<CardViewProps> = ({
|
||||
|
||||
<div className="bookmark-footer">
|
||||
<div className="bookmark-meta-minimal">
|
||||
<a
|
||||
href={getProfileUrl(authorNpub)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
<Link
|
||||
to={`/p/${authorNpub}`}
|
||||
className="author-link-minimal"
|
||||
title="Open author in search"
|
||||
title="Open author profile"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{getAuthorDisplayName()}
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
{/* CTA removed */}
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import { IndividualBookmark } from '../../types/bookmarks'
|
||||
import { formatDate } from '../../utils/bookmarkUtils'
|
||||
@@ -6,7 +7,7 @@ import ContentWithResolvedProfiles from '../ContentWithResolvedProfiles'
|
||||
import { IconGetter } from './shared'
|
||||
import { useImageCache } from '../../hooks/useImageCache'
|
||||
import { UserSettings } from '../../services/settingsService'
|
||||
import { getProfileUrl, getEventUrl } from '../../config/nostrGateways'
|
||||
import { getEventUrl } from '../../config/nostrGateways'
|
||||
|
||||
interface LargeViewProps {
|
||||
bookmark: IndividualBookmark
|
||||
@@ -93,15 +94,13 @@ export const LargeView: React.FC<LargeViewProps> = ({
|
||||
|
||||
<div className="large-footer">
|
||||
<span className="large-author">
|
||||
<a
|
||||
href={getProfileUrl(authorNpub)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
<Link
|
||||
to={`/p/${authorNpub}`}
|
||||
className="author-link-minimal"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{getAuthorDisplayName()}
|
||||
</a>
|
||||
</Link>
|
||||
</span>
|
||||
|
||||
{eventNevent && (
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useParams, useLocation, useNavigate } from 'react-router-dom'
|
||||
import { Hooks } from 'applesauce-react'
|
||||
import { useEventStore } from 'applesauce-react/hooks'
|
||||
import { RelayPool } from 'applesauce-relay'
|
||||
import { nip19 } from 'nostr-tools'
|
||||
import { useSettings } from '../hooks/useSettings'
|
||||
import { useArticleLoader } from '../hooks/useArticleLoader'
|
||||
import { useExternalUrlLoader } from '../hooks/useExternalUrlLoader'
|
||||
@@ -25,7 +26,7 @@ interface BookmarksProps {
|
||||
}
|
||||
|
||||
const Bookmarks: React.FC<BookmarksProps> = ({ relayPool, onLogout }) => {
|
||||
const { naddr } = useParams<{ naddr?: string }>()
|
||||
const { naddr, npub } = useParams<{ naddr?: string; npub?: string }>()
|
||||
const location = useLocation()
|
||||
const navigate = useNavigate()
|
||||
const previousLocationRef = useRef<string>()
|
||||
@@ -37,6 +38,7 @@ const Bookmarks: React.FC<BookmarksProps> = ({ relayPool, onLogout }) => {
|
||||
const showSettings = location.pathname === '/settings'
|
||||
const showExplore = location.pathname === '/explore'
|
||||
const showMe = location.pathname.startsWith('/me')
|
||||
const showProfile = location.pathname.startsWith('/p/')
|
||||
|
||||
// Extract tab from me routes
|
||||
const meTab = location.pathname === '/me' ? 'highlights' :
|
||||
@@ -45,12 +47,28 @@ const Bookmarks: React.FC<BookmarksProps> = ({ relayPool, onLogout }) => {
|
||||
location.pathname === '/me/archive' ? 'archive' :
|
||||
location.pathname === '/me/writings' ? 'writings' : 'highlights'
|
||||
|
||||
// Track previous location for going back from settings/me/explore
|
||||
// Extract tab from profile routes
|
||||
const profileTab = location.pathname.endsWith('/writings') ? 'writings' : 'highlights'
|
||||
|
||||
// Decode npub to pubkey for profile view
|
||||
let profilePubkey: string | undefined
|
||||
if (npub && showProfile) {
|
||||
try {
|
||||
const decoded = nip19.decode(npub)
|
||||
if (decoded.type === 'npub') {
|
||||
profilePubkey = decoded.data
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to decode npub:', err)
|
||||
}
|
||||
}
|
||||
|
||||
// Track previous location for going back from settings/me/explore/profile
|
||||
useEffect(() => {
|
||||
if (!showSettings && !showMe && !showExplore) {
|
||||
if (!showSettings && !showMe && !showExplore && !showProfile) {
|
||||
previousLocationRef.current = location.pathname
|
||||
}
|
||||
}, [location.pathname, showSettings, showMe, showExplore])
|
||||
}, [location.pathname, showSettings, showMe, showExplore, showProfile])
|
||||
|
||||
const activeAccount = Hooks.useActiveAccount()
|
||||
const accountManager = Hooks.useAccountManager()
|
||||
@@ -212,6 +230,7 @@ const Bookmarks: React.FC<BookmarksProps> = ({ relayPool, onLogout }) => {
|
||||
showSettings={showSettings}
|
||||
showExplore={showExplore}
|
||||
showMe={showMe}
|
||||
showProfile={showProfile}
|
||||
bookmarks={bookmarks}
|
||||
bookmarksLoading={bookmarksLoading}
|
||||
viewMode={viewMode}
|
||||
@@ -272,6 +291,9 @@ const Bookmarks: React.FC<BookmarksProps> = ({ relayPool, onLogout }) => {
|
||||
me={showMe ? (
|
||||
relayPool ? <Me relayPool={relayPool} activeTab={meTab} /> : null
|
||||
) : undefined}
|
||||
profile={showProfile && profilePubkey ? (
|
||||
relayPool ? <Me relayPool={relayPool} activeTab={profileTab} pubkey={profilePubkey} /> : null
|
||||
) : undefined}
|
||||
toastMessage={toastMessage ?? undefined}
|
||||
toastType={toastType}
|
||||
onClearToast={clearToast}
|
||||
|
||||
@@ -40,8 +40,11 @@ const HighlightsPanelHeader: React.FC<HighlightsPanelHeaderProps> = ({
|
||||
})}
|
||||
title="Toggle nostrverse highlights"
|
||||
ariaLabel="Toggle nostrverse highlights"
|
||||
variant={highlightVisibility.nostrverse ? 'primary' : 'ghost'}
|
||||
style={{ color: highlightVisibility.nostrverse ? 'var(--highlight-color-nostrverse, #9333ea)' : undefined }}
|
||||
variant="ghost"
|
||||
style={{
|
||||
color: highlightVisibility.nostrverse ? 'var(--highlight-color-nostrverse, #9333ea)' : undefined,
|
||||
opacity: highlightVisibility.nostrverse ? 1 : 0.4
|
||||
}}
|
||||
/>
|
||||
<IconButton
|
||||
icon={faUserGroup}
|
||||
@@ -51,9 +54,12 @@ const HighlightsPanelHeader: React.FC<HighlightsPanelHeaderProps> = ({
|
||||
})}
|
||||
title={currentUserPubkey ? "Toggle friends highlights" : "Login to see friends highlights"}
|
||||
ariaLabel="Toggle friends highlights"
|
||||
variant={highlightVisibility.friends ? 'primary' : 'ghost'}
|
||||
variant="ghost"
|
||||
disabled={!currentUserPubkey}
|
||||
style={{ color: highlightVisibility.friends ? 'var(--highlight-color-friends, #f97316)' : undefined }}
|
||||
style={{
|
||||
color: highlightVisibility.friends ? 'var(--highlight-color-friends, #f97316)' : undefined,
|
||||
opacity: highlightVisibility.friends ? 1 : 0.4
|
||||
}}
|
||||
/>
|
||||
<IconButton
|
||||
icon={faUser}
|
||||
@@ -63,9 +69,12 @@ const HighlightsPanelHeader: React.FC<HighlightsPanelHeaderProps> = ({
|
||||
})}
|
||||
title={currentUserPubkey ? "Toggle my highlights" : "Login to see your highlights"}
|
||||
ariaLabel="Toggle my highlights"
|
||||
variant={highlightVisibility.mine ? 'primary' : 'ghost'}
|
||||
variant="ghost"
|
||||
disabled={!currentUserPubkey}
|
||||
style={{ color: highlightVisibility.mine ? 'var(--highlight-color-mine, #eab308)' : undefined }}
|
||||
style={{
|
||||
color: highlightVisibility.mine ? 'var(--highlight-color-mine, #eab308)' : undefined,
|
||||
opacity: highlightVisibility.mine ? 1 : 0.4
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -23,18 +23,24 @@ import { getCachedMeData, setCachedMeData, updateCachedHighlights } from '../ser
|
||||
import { faBooks } from '../icons/customIcons'
|
||||
import { usePullToRefresh } from '../hooks/usePullToRefresh'
|
||||
import PullToRefreshIndicator from './PullToRefreshIndicator'
|
||||
import { getProfileUrl } from '../config/nostrGateways'
|
||||
|
||||
interface MeProps {
|
||||
relayPool: RelayPool
|
||||
activeTab?: TabType
|
||||
pubkey?: string // Optional pubkey for viewing other users' profiles
|
||||
}
|
||||
|
||||
type TabType = 'highlights' | 'reading-list' | 'archive' | 'writings'
|
||||
|
||||
const Me: React.FC<MeProps> = ({ relayPool, activeTab: propActiveTab }) => {
|
||||
const Me: React.FC<MeProps> = ({ relayPool, activeTab: propActiveTab, pubkey: propPubkey }) => {
|
||||
const activeAccount = Hooks.useActiveAccount()
|
||||
const navigate = useNavigate()
|
||||
const [activeTab, setActiveTab] = useState<TabType>(propActiveTab || 'highlights')
|
||||
|
||||
// Use provided pubkey or fall back to active account
|
||||
const viewingPubkey = propPubkey || activeAccount?.pubkey
|
||||
const isOwnProfile = !propPubkey || (activeAccount?.pubkey === propPubkey)
|
||||
const [highlights, setHighlights] = useState<Highlight[]>([])
|
||||
const [bookmarks, setBookmarks] = useState<Bookmark[]>([])
|
||||
const [readArticles, setReadArticles] = useState<BlogPostPreview[]>([])
|
||||
@@ -54,8 +60,8 @@ const Me: React.FC<MeProps> = ({ relayPool, activeTab: propActiveTab }) => {
|
||||
|
||||
useEffect(() => {
|
||||
const loadData = async () => {
|
||||
if (!activeAccount) {
|
||||
setError('Please log in to view your data')
|
||||
if (!viewingPubkey) {
|
||||
setError(isOwnProfile ? 'Please log in to view your data' : 'Invalid profile')
|
||||
setLoading(false)
|
||||
return
|
||||
}
|
||||
@@ -64,39 +70,48 @@ const Me: React.FC<MeProps> = ({ relayPool, activeTab: propActiveTab }) => {
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
|
||||
// Seed from cache if available to avoid empty flash
|
||||
const cached = getCachedMeData(activeAccount.pubkey)
|
||||
if (cached) {
|
||||
setHighlights(cached.highlights)
|
||||
setBookmarks(cached.bookmarks)
|
||||
setReadArticles(cached.readArticles)
|
||||
// Seed from cache if available to avoid empty flash (own profile only)
|
||||
if (isOwnProfile) {
|
||||
const cached = getCachedMeData(viewingPubkey)
|
||||
if (cached) {
|
||||
setHighlights(cached.highlights)
|
||||
setBookmarks(cached.bookmarks)
|
||||
setReadArticles(cached.readArticles)
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch highlights, read articles, and writings
|
||||
const [userHighlights, userReadArticles, userWritings] = await Promise.all([
|
||||
fetchHighlights(relayPool, activeAccount.pubkey),
|
||||
fetchReadArticlesWithData(relayPool, activeAccount.pubkey),
|
||||
fetchBlogPostsFromAuthors(relayPool, [activeAccount.pubkey], RELAYS)
|
||||
// Fetch highlights and writings (public data)
|
||||
const [userHighlights, userWritings] = await Promise.all([
|
||||
fetchHighlights(relayPool, viewingPubkey),
|
||||
fetchBlogPostsFromAuthors(relayPool, [viewingPubkey], RELAYS)
|
||||
])
|
||||
|
||||
setHighlights(userHighlights)
|
||||
setReadArticles(userReadArticles)
|
||||
setWritings(userWritings)
|
||||
|
||||
// Fetch bookmarks using callback pattern
|
||||
let fetchedBookmarks: Bookmark[] = []
|
||||
try {
|
||||
await fetchBookmarks(relayPool, activeAccount, (newBookmarks) => {
|
||||
fetchedBookmarks = newBookmarks
|
||||
setBookmarks(newBookmarks)
|
||||
})
|
||||
} catch (err) {
|
||||
console.warn('Failed to load bookmarks:', err)
|
||||
setBookmarks([])
|
||||
}
|
||||
// Only fetch private data for own profile
|
||||
if (isOwnProfile && activeAccount) {
|
||||
const userReadArticles = await fetchReadArticlesWithData(relayPool, viewingPubkey)
|
||||
setReadArticles(userReadArticles)
|
||||
|
||||
// Update cache with all fetched data
|
||||
setCachedMeData(activeAccount.pubkey, userHighlights, fetchedBookmarks, userReadArticles)
|
||||
// Fetch bookmarks using callback pattern
|
||||
let fetchedBookmarks: Bookmark[] = []
|
||||
try {
|
||||
await fetchBookmarks(relayPool, activeAccount, (newBookmarks) => {
|
||||
fetchedBookmarks = newBookmarks
|
||||
setBookmarks(newBookmarks)
|
||||
})
|
||||
} catch (err) {
|
||||
console.warn('Failed to load bookmarks:', err)
|
||||
setBookmarks([])
|
||||
}
|
||||
|
||||
// Update cache with all fetched data
|
||||
setCachedMeData(viewingPubkey, userHighlights, fetchedBookmarks, userReadArticles)
|
||||
} else {
|
||||
setBookmarks([])
|
||||
setReadArticles([])
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to load data:', err)
|
||||
setError('Failed to load data. Please try again.')
|
||||
@@ -106,7 +121,7 @@ const Me: React.FC<MeProps> = ({ relayPool, activeTab: propActiveTab }) => {
|
||||
}
|
||||
|
||||
loadData()
|
||||
}, [relayPool, activeAccount, refreshTrigger])
|
||||
}, [relayPool, viewingPubkey, isOwnProfile, activeAccount, refreshTrigger])
|
||||
|
||||
// Pull-to-refresh
|
||||
const pullToRefreshState = usePullToRefresh(meContainerRef, {
|
||||
@@ -119,9 +134,9 @@ const Me: React.FC<MeProps> = ({ relayPool, activeTab: propActiveTab }) => {
|
||||
const handleHighlightDelete = (highlightId: string) => {
|
||||
setHighlights(prev => {
|
||||
const updated = prev.filter(h => h.id !== highlightId)
|
||||
// Update cache when highlight is deleted
|
||||
if (activeAccount) {
|
||||
updateCachedHighlights(activeAccount.pubkey, updated)
|
||||
// Update cache when highlight is deleted (own profile only)
|
||||
if (isOwnProfile && viewingPubkey) {
|
||||
updateCachedHighlights(viewingPubkey, updated)
|
||||
}
|
||||
return updated
|
||||
})
|
||||
@@ -206,8 +221,12 @@ const Me: React.FC<MeProps> = ({ relayPool, activeTab: propActiveTab }) => {
|
||||
switch (activeTab) {
|
||||
case 'highlights':
|
||||
return highlights.length === 0 ? (
|
||||
<div className="explore-error">
|
||||
<p>No highlights yet. Start highlighting content to see them here!</p>
|
||||
<div className="explore-empty">
|
||||
<p>
|
||||
{isOwnProfile
|
||||
? 'No highlights yet. Start highlighting content to see them here!'
|
||||
: 'No highlights yet. You should shame them on nostr!'}
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="highlights-list me-highlights-list">
|
||||
@@ -224,7 +243,7 @@ const Me: React.FC<MeProps> = ({ relayPool, activeTab: propActiveTab }) => {
|
||||
|
||||
case 'reading-list':
|
||||
return allIndividualBookmarks.length === 0 ? (
|
||||
<div className="explore-error">
|
||||
<div className="explore-empty">
|
||||
<p>No bookmarks yet. Bookmark articles to see them here!</p>
|
||||
</div>
|
||||
) : (
|
||||
@@ -275,7 +294,7 @@ const Me: React.FC<MeProps> = ({ relayPool, activeTab: propActiveTab }) => {
|
||||
|
||||
case 'archive':
|
||||
return readArticles.length === 0 ? (
|
||||
<div className="explore-error">
|
||||
<div className="explore-empty">
|
||||
<p>No read articles yet. Mark articles as read to see them here!</p>
|
||||
</div>
|
||||
) : (
|
||||
@@ -292,8 +311,25 @@ const Me: React.FC<MeProps> = ({ relayPool, activeTab: propActiveTab }) => {
|
||||
|
||||
case 'writings':
|
||||
return writings.length === 0 ? (
|
||||
<div className="explore-error">
|
||||
<p>No articles written yet. Publish your first article to see it here!</p>
|
||||
<div className="explore-empty">
|
||||
<p>
|
||||
{isOwnProfile
|
||||
? 'No articles written yet. Publish your first article to see it here!'
|
||||
: (
|
||||
<>
|
||||
No articles written. You can find other stuff from this user using{' '}
|
||||
<a
|
||||
href={viewingPubkey ? getProfileUrl(nip19.npubEncode(viewingPubkey)) : '#'}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
style={{ color: 'rgb(99 102 241)', textDecoration: 'underline' }}
|
||||
>
|
||||
ants
|
||||
</a>
|
||||
.
|
||||
</>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="explore-grid">
|
||||
@@ -324,7 +360,7 @@ const Me: React.FC<MeProps> = ({ relayPool, activeTab: propActiveTab }) => {
|
||||
isRefreshing={loading && pullToRefreshState.canRefresh}
|
||||
/>
|
||||
<div className="explore-header">
|
||||
{activeAccount && <AuthorCard authorPubkey={activeAccount.pubkey} />}
|
||||
{viewingPubkey && <AuthorCard authorPubkey={viewingPubkey} clickable={false} />}
|
||||
|
||||
{loading && hasData && (
|
||||
<div className="explore-loading" style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', padding: '0.5rem 0' }}>
|
||||
@@ -336,38 +372,38 @@ const Me: React.FC<MeProps> = ({ relayPool, activeTab: propActiveTab }) => {
|
||||
<button
|
||||
className={`me-tab ${activeTab === 'highlights' ? 'active' : ''}`}
|
||||
data-tab="highlights"
|
||||
onClick={() => navigate('/me/highlights')}
|
||||
onClick={() => navigate(isOwnProfile ? '/me/highlights' : `/p/${propPubkey && nip19.npubEncode(propPubkey)}`)}
|
||||
>
|
||||
<FontAwesomeIcon icon={faHighlighter} />
|
||||
<span className="tab-label">Highlights</span>
|
||||
<span className="tab-count">({highlights.length})</span>
|
||||
</button>
|
||||
<button
|
||||
className={`me-tab ${activeTab === 'reading-list' ? 'active' : ''}`}
|
||||
data-tab="reading-list"
|
||||
onClick={() => navigate('/me/reading-list')}
|
||||
>
|
||||
<FontAwesomeIcon icon={faBookmark} />
|
||||
<span className="tab-label">Reading List</span>
|
||||
<span className="tab-count">({allIndividualBookmarks.length})</span>
|
||||
</button>
|
||||
<button
|
||||
className={`me-tab ${activeTab === 'archive' ? 'active' : ''}`}
|
||||
data-tab="archive"
|
||||
onClick={() => navigate('/me/archive')}
|
||||
>
|
||||
<FontAwesomeIcon icon={faBooks} />
|
||||
<span className="tab-label">Archive</span>
|
||||
<span className="tab-count">({readArticles.length})</span>
|
||||
</button>
|
||||
{isOwnProfile && (
|
||||
<>
|
||||
<button
|
||||
className={`me-tab ${activeTab === 'reading-list' ? 'active' : ''}`}
|
||||
data-tab="reading-list"
|
||||
onClick={() => navigate('/me/reading-list')}
|
||||
>
|
||||
<FontAwesomeIcon icon={faBookmark} />
|
||||
<span className="tab-label">Bookmarks</span>
|
||||
</button>
|
||||
<button
|
||||
className={`me-tab ${activeTab === 'archive' ? 'active' : ''}`}
|
||||
data-tab="archive"
|
||||
onClick={() => navigate('/me/archive')}
|
||||
>
|
||||
<FontAwesomeIcon icon={faBooks} />
|
||||
<span className="tab-label">Archive</span>
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
<button
|
||||
className={`me-tab ${activeTab === 'writings' ? 'active' : ''}`}
|
||||
data-tab="writings"
|
||||
onClick={() => navigate('/me/writings')}
|
||||
onClick={() => navigate(isOwnProfile ? '/me/writings' : `/p/${propPubkey && nip19.npubEncode(propPubkey)}/writings`)}
|
||||
>
|
||||
<FontAwesomeIcon icon={faPenToSquare} />
|
||||
<span className="tab-label">Writings</span>
|
||||
<span className="tab-count">({writings.length})</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import { faArrowDown, faSpinner } from '@fortawesome/free-solid-svg-icons'
|
||||
import { faArrowDown } from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
interface PullToRefreshIndicatorProps {
|
||||
isPulling: boolean
|
||||
@@ -14,11 +14,10 @@ const PullToRefreshIndicator: React.FC<PullToRefreshIndicatorProps> = ({
|
||||
isPulling,
|
||||
pullDistance,
|
||||
canRefresh,
|
||||
isRefreshing,
|
||||
threshold = 80
|
||||
}) => {
|
||||
// Don't show if not pulling and not refreshing
|
||||
if (!isPulling && !isRefreshing) return null
|
||||
// Only show when actively pulling, not when refreshing
|
||||
if (!isPulling) return null
|
||||
|
||||
const opacity = Math.min(pullDistance / threshold, 1)
|
||||
const rotation = (pullDistance / threshold) * 180
|
||||
@@ -27,31 +26,23 @@ const PullToRefreshIndicator: React.FC<PullToRefreshIndicatorProps> = ({
|
||||
<div
|
||||
className="pull-to-refresh-indicator"
|
||||
style={{
|
||||
opacity: isRefreshing ? 1 : opacity,
|
||||
transform: `translateY(${isRefreshing ? 0 : -20 + pullDistance / 2}px)`
|
||||
opacity,
|
||||
transform: `translateY(${-20 + pullDistance / 2}px)`
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="pull-to-refresh-icon"
|
||||
style={{
|
||||
transform: isRefreshing ? 'none' : `rotate(${rotation}deg)`
|
||||
transform: `rotate(${rotation}deg)`
|
||||
}}
|
||||
>
|
||||
{isRefreshing ? (
|
||||
<FontAwesomeIcon icon={faSpinner} spin />
|
||||
) : (
|
||||
<FontAwesomeIcon
|
||||
icon={faArrowDown}
|
||||
style={{ color: canRefresh ? 'var(--accent-color, #3b82f6)' : 'var(--text-secondary)' }}
|
||||
/>
|
||||
)}
|
||||
<FontAwesomeIcon
|
||||
icon={faArrowDown}
|
||||
style={{ color: canRefresh ? 'var(--accent-color, #3b82f6)' : 'var(--text-secondary)' }}
|
||||
/>
|
||||
</div>
|
||||
<div className="pull-to-refresh-text">
|
||||
{isRefreshing
|
||||
? 'Refreshing...'
|
||||
: canRefresh
|
||||
? 'Release to refresh'
|
||||
: 'Pull to refresh'}
|
||||
{canRefresh ? 'Release to refresh' : 'Pull to refresh'}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -29,28 +29,39 @@ export const ReadingProgressIndicator: React.FC<ReadingProgressIndicatorProps> =
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`reading-progress-bar fixed bottom-0 left-0 right-0 z-[1102] bg-[rgba(26,26,26,0.85)] backdrop-blur-sm px-3 py-1 flex items-center gap-2 transition-all duration-300 ${className}`}
|
||||
className={`reading-progress-bar fixed bottom-0 left-0 right-0 z-[1102] backdrop-blur-sm px-3 py-1 flex items-center gap-2 transition-all duration-300 ${className}`}
|
||||
style={{
|
||||
'--left-offset': leftOffset,
|
||||
'--right-offset': rightOffset
|
||||
'--right-offset': rightOffset,
|
||||
backgroundColor: 'var(--color-bg-elevated)',
|
||||
opacity: 0.95
|
||||
} as React.CSSProperties}
|
||||
>
|
||||
<div className="flex-1 h-0.5 bg-white/10 rounded-full overflow-hidden relative">
|
||||
<div
|
||||
className="flex-1 h-0.5 rounded-full overflow-hidden relative"
|
||||
style={{ backgroundColor: 'var(--color-border)' }}
|
||||
>
|
||||
<div
|
||||
className={`h-full rounded-full transition-all duration-300 relative ${
|
||||
isComplete
|
||||
? 'bg-green-500'
|
||||
: 'bg-indigo-500'
|
||||
: ''
|
||||
}`}
|
||||
style={{ width: `${clampedProgress}%` }}
|
||||
style={{
|
||||
width: `${clampedProgress}%`,
|
||||
backgroundColor: isComplete ? undefined : 'var(--color-primary)'
|
||||
}}
|
||||
>
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-white/30 to-transparent animate-[shimmer_2s_infinite]" />
|
||||
</div>
|
||||
</div>
|
||||
{showPercentage && (
|
||||
<div className={`text-[0.625rem] font-normal min-w-[32px] text-right tabular-nums ${
|
||||
isComplete ? 'text-green-500' : 'text-gray-500'
|
||||
}`}>
|
||||
<div
|
||||
className={`text-[0.625rem] font-normal min-w-[32px] text-right tabular-nums ${
|
||||
isComplete ? 'text-green-500' : ''
|
||||
}`}
|
||||
style={{ color: isComplete ? undefined : 'var(--color-text-muted)' }}
|
||||
>
|
||||
{isComplete ? '✓' : `${clampedProgress}%`}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -70,8 +70,12 @@ export const RelayStatusIndicator: React.FC<RelayStatusIndicatorProps> = ({
|
||||
}
|
||||
}
|
||||
|
||||
// On mobile, default to collapsed (icon only). On desktop, always show details.
|
||||
const showDetails = !isMobile || isExpanded
|
||||
|
||||
// On mobile when collapsed, make it circular like the highlight button
|
||||
const isCollapsedOnMobile = isMobile && !isExpanded
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`relay-status-indicator ${isConnecting ? 'connecting' : ''} ${isMobile ? 'mobile' : ''} ${isExpanded ? 'expanded' : ''} ${isMobile && !showOnMobile ? 'hidden' : 'visible'}`}
|
||||
@@ -85,25 +89,75 @@ export const RelayStatusIndicator: React.FC<RelayStatusIndicatorProps> = ({
|
||||
) : undefined
|
||||
}
|
||||
onClick={handleClick}
|
||||
style={{ cursor: isMobile ? 'pointer' : 'default' }}
|
||||
style={{
|
||||
position: 'fixed',
|
||||
bottom: '32px',
|
||||
left: '32px',
|
||||
zIndex: 1000,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: showDetails ? '0.5rem' : '0',
|
||||
padding: isCollapsedOnMobile ? '0.875rem' : (showDetails ? '0.75rem 1rem' : '0.75rem'),
|
||||
width: isCollapsedOnMobile ? '56px' : 'auto',
|
||||
height: isCollapsedOnMobile ? '56px' : 'auto',
|
||||
backgroundColor: 'rgba(39, 39, 42, 0.9)',
|
||||
backdropFilter: 'blur(8px)',
|
||||
border: '1px solid rgb(82, 82, 91)',
|
||||
borderRadius: isCollapsedOnMobile ? '50%' : '12px',
|
||||
color: 'rgb(228, 228, 231)',
|
||||
fontSize: '0.875rem',
|
||||
fontWeight: 500,
|
||||
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.3)',
|
||||
cursor: isMobile ? 'pointer' : 'default',
|
||||
opacity: isMobile && !showOnMobile ? 0 : 1,
|
||||
visibility: isMobile && !showOnMobile ? 'hidden' : 'visible',
|
||||
transition: 'all 0.3s ease',
|
||||
userSelect: 'none',
|
||||
justifyContent: isCollapsedOnMobile ? 'center' : 'flex-start'
|
||||
}}
|
||||
>
|
||||
<div className="relay-status-icon">
|
||||
<FontAwesomeIcon icon={isConnecting ? faSpinner : offlineMode ? faCircle : faPlane} spin={isConnecting} />
|
||||
</div>
|
||||
{showDetails && (
|
||||
<>
|
||||
<div className="relay-status-text">
|
||||
<div
|
||||
className="relay-status-text"
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '0.125rem'
|
||||
}}
|
||||
>
|
||||
{isConnecting ? (
|
||||
<span className="relay-status-title">Connecting</span>
|
||||
) : offlineMode ? (
|
||||
<>
|
||||
<span className="relay-status-title">Offline</span>
|
||||
<span className="relay-status-subtitle">No relays connected</span>
|
||||
<span
|
||||
className="relay-status-subtitle"
|
||||
style={{
|
||||
fontSize: '0.75rem',
|
||||
opacity: 0.7,
|
||||
fontWeight: 400
|
||||
}}
|
||||
>
|
||||
No relays connected
|
||||
</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<span className="relay-status-title">Flight Mode</span>
|
||||
<span className="relay-status-subtitle">{connectedUrls.length} local relay{connectedUrls.length !== 1 ? 's' : ''}</span>
|
||||
<span
|
||||
className="relay-status-subtitle"
|
||||
style={{
|
||||
fontSize: '0.75rem',
|
||||
opacity: 0.7,
|
||||
fontWeight: 400
|
||||
}}
|
||||
>
|
||||
{connectedUrls.length} local relay{connectedUrls.length !== 1 ? 's' : ''}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import React from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { useEventModel } from 'applesauce-react/hooks'
|
||||
import { Models, Helpers } from 'applesauce-core'
|
||||
import { decode, npubEncode } from 'nostr-tools/nip19'
|
||||
import { getProfileUrl } from '../config/nostrGateways'
|
||||
|
||||
const { getPubkeyFromDecodeResult } = Helpers
|
||||
|
||||
@@ -25,14 +25,12 @@ const ResolvedMention: React.FC<ResolvedMentionProps> = ({ encoded }) => {
|
||||
|
||||
if (npub) {
|
||||
return (
|
||||
<a
|
||||
href={getProfileUrl(npub)}
|
||||
<Link
|
||||
to={`/p/${npub}`}
|
||||
className="nostr-mention"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
@{display}
|
||||
</a>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import { RelayPool } from 'applesauce-relay'
|
||||
import { UserSettings } from '../services/settingsService'
|
||||
import IconButton from './IconButton'
|
||||
import { loadFont } from '../utils/fontLoader'
|
||||
import ThemeSettings from './Settings/ThemeSettings'
|
||||
import ReadingDisplaySettings from './Settings/ReadingDisplaySettings'
|
||||
import LayoutNavigationSettings from './Settings/LayoutNavigationSettings'
|
||||
import StartupPreferencesSettings from './Settings/StartupPreferencesSettings'
|
||||
@@ -159,6 +160,7 @@ const Settings: React.FC<SettingsProps> = ({ settings, onSave, onClose, relayPoo
|
||||
</div>
|
||||
|
||||
<div className="settings-content">
|
||||
<ThemeSettings settings={localSettings} onUpdate={handleUpdate} />
|
||||
<ReadingDisplaySettings settings={localSettings} onUpdate={handleUpdate} />
|
||||
<LayoutNavigationSettings settings={localSettings} onUpdate={handleUpdate} />
|
||||
<StartupPreferencesSettings settings={localSettings} onUpdate={handleUpdate} />
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import React from 'react'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import { faHighlighter, faUnderline, faNetworkWired, faUserGroup, faUser } from '@fortawesome/free-solid-svg-icons'
|
||||
import { UserSettings } from '../../services/settingsService'
|
||||
import IconButton from '../IconButton'
|
||||
@@ -102,33 +101,39 @@ const ReadingDisplaySettings: React.FC<ReadingDisplaySettingsProps> = ({ setting
|
||||
<div className="setting-group setting-inline">
|
||||
<label>Default Highlight Visibility</label>
|
||||
<div className="highlight-level-toggles">
|
||||
<button
|
||||
<IconButton
|
||||
icon={faNetworkWired}
|
||||
onClick={() => onUpdate({ defaultHighlightVisibilityNostrverse: !(settings.defaultHighlightVisibilityNostrverse !== false) })}
|
||||
className={`level-toggle-btn ${(settings.defaultHighlightVisibilityNostrverse !== false) ? 'active' : ''}`}
|
||||
title="Nostrverse highlights"
|
||||
aria-label="Toggle nostrverse highlights by default"
|
||||
style={{ color: (settings.defaultHighlightVisibilityNostrverse !== false) ? 'var(--highlight-color-nostrverse, #9333ea)' : undefined }}
|
||||
>
|
||||
<FontAwesomeIcon icon={faNetworkWired} />
|
||||
</button>
|
||||
<button
|
||||
ariaLabel="Toggle nostrverse highlights by default"
|
||||
variant="ghost"
|
||||
style={{
|
||||
color: (settings.defaultHighlightVisibilityNostrverse !== false) ? 'var(--highlight-color-nostrverse, #9333ea)' : undefined,
|
||||
opacity: (settings.defaultHighlightVisibilityNostrverse !== false) ? 1 : 0.4
|
||||
}}
|
||||
/>
|
||||
<IconButton
|
||||
icon={faUserGroup}
|
||||
onClick={() => onUpdate({ defaultHighlightVisibilityFriends: !(settings.defaultHighlightVisibilityFriends !== false) })}
|
||||
className={`level-toggle-btn ${(settings.defaultHighlightVisibilityFriends !== false) ? 'active' : ''}`}
|
||||
title="Friends highlights"
|
||||
aria-label="Toggle friends highlights by default"
|
||||
style={{ color: (settings.defaultHighlightVisibilityFriends !== false) ? 'var(--highlight-color-friends, #f97316)' : undefined }}
|
||||
>
|
||||
<FontAwesomeIcon icon={faUserGroup} />
|
||||
</button>
|
||||
<button
|
||||
ariaLabel="Toggle friends highlights by default"
|
||||
variant="ghost"
|
||||
style={{
|
||||
color: (settings.defaultHighlightVisibilityFriends !== false) ? 'var(--highlight-color-friends, #f97316)' : undefined,
|
||||
opacity: (settings.defaultHighlightVisibilityFriends !== false) ? 1 : 0.4
|
||||
}}
|
||||
/>
|
||||
<IconButton
|
||||
icon={faUser}
|
||||
onClick={() => onUpdate({ defaultHighlightVisibilityMine: !(settings.defaultHighlightVisibilityMine !== false) })}
|
||||
className={`level-toggle-btn ${(settings.defaultHighlightVisibilityMine !== false) ? 'active' : ''}`}
|
||||
title="My highlights"
|
||||
aria-label="Toggle my highlights by default"
|
||||
style={{ color: (settings.defaultHighlightVisibilityMine !== false) ? 'var(--highlight-color-mine, #eab308)' : undefined }}
|
||||
>
|
||||
<FontAwesomeIcon icon={faUser} />
|
||||
</button>
|
||||
ariaLabel="Toggle my highlights by default"
|
||||
variant="ghost"
|
||||
style={{
|
||||
color: (settings.defaultHighlightVisibilityMine !== false) ? 'var(--highlight-color-mine, #eab308)' : undefined,
|
||||
opacity: (settings.defaultHighlightVisibilityMine !== false) ? 1 : 0.4
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
107
src/components/Settings/ThemeSettings.tsx
Normal file
107
src/components/Settings/ThemeSettings.tsx
Normal file
@@ -0,0 +1,107 @@
|
||||
import React from 'react'
|
||||
import { faSun, faMoon, faDesktop } from '@fortawesome/free-solid-svg-icons'
|
||||
import { UserSettings } from '../../services/settingsService'
|
||||
import IconButton from '../IconButton'
|
||||
|
||||
type DarkColorTheme = 'black' | 'midnight' | 'charcoal'
|
||||
type LightColorTheme = 'paper-white' | 'sepia' | 'ivory'
|
||||
|
||||
interface ThemeSettingsProps {
|
||||
settings: UserSettings
|
||||
onUpdate: (updates: Partial<UserSettings>) => void
|
||||
}
|
||||
|
||||
const ThemeSettings: React.FC<ThemeSettingsProps> = ({ settings, onUpdate }) => {
|
||||
const currentTheme = settings.theme ?? 'system'
|
||||
const currentDarkColor = settings.darkColorTheme ?? 'midnight'
|
||||
const currentLightColor = settings.lightColorTheme ?? 'sepia'
|
||||
|
||||
// Determine which color picker to show based on current theme
|
||||
const showDarkColors = currentTheme === 'dark' || currentTheme === 'system'
|
||||
const showLightColors = currentTheme === 'light' || currentTheme === 'system'
|
||||
|
||||
// Color definitions for swatches
|
||||
const darkColors = {
|
||||
black: '#000000',
|
||||
midnight: '#18181b',
|
||||
charcoal: '#1c1c1e'
|
||||
}
|
||||
|
||||
const lightColors = {
|
||||
'paper-white': '#ffffff',
|
||||
sepia: '#f4f1ea',
|
||||
ivory: '#fffff0'
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="settings-section">
|
||||
<h3 className="section-title">Theme</h3>
|
||||
|
||||
<div className="setting-group setting-inline">
|
||||
<label>Appearance</label>
|
||||
<div className="setting-buttons">
|
||||
<IconButton
|
||||
icon={faSun}
|
||||
onClick={() => onUpdate({ theme: 'light' })}
|
||||
title="Light theme"
|
||||
ariaLabel="Light theme"
|
||||
variant={currentTheme === 'light' ? 'primary' : 'ghost'}
|
||||
/>
|
||||
<IconButton
|
||||
icon={faMoon}
|
||||
onClick={() => onUpdate({ theme: 'dark' })}
|
||||
title="Dark theme"
|
||||
ariaLabel="Dark theme"
|
||||
variant={currentTheme === 'dark' ? 'primary' : 'ghost'}
|
||||
/>
|
||||
<IconButton
|
||||
icon={faDesktop}
|
||||
onClick={() => onUpdate({ theme: 'system' })}
|
||||
title="Use system preference"
|
||||
ariaLabel="Use system preference"
|
||||
variant={currentTheme === 'system' ? 'primary' : 'ghost'}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{showDarkColors && (
|
||||
<div className="setting-group setting-inline">
|
||||
<label>Dark Theme</label>
|
||||
<div className="color-picker">
|
||||
{Object.entries(darkColors).map(([key, color]) => (
|
||||
<div
|
||||
key={key}
|
||||
className={`color-swatch ${currentDarkColor === key ? 'active' : ''}`}
|
||||
style={{ backgroundColor: color }}
|
||||
onClick={() => onUpdate({ darkColorTheme: key as DarkColorTheme })}
|
||||
title={key.charAt(0).toUpperCase() + key.slice(1)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showLightColors && (
|
||||
<div className="setting-group setting-inline">
|
||||
<label>Light Theme</label>
|
||||
<div className="color-picker">
|
||||
{Object.entries(lightColors).map(([key, color]) => (
|
||||
<div
|
||||
key={key}
|
||||
className={`color-swatch ${currentLightColor === key ? 'active' : ''}`}
|
||||
style={{
|
||||
backgroundColor: color,
|
||||
border: color === '#ffffff' ? '2px solid #e5e7eb' : '1px solid #e5e7eb'
|
||||
}}
|
||||
onClick={() => onUpdate({ lightColorTheme: key as LightColorTheme })}
|
||||
title={key.split('-').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' ')}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ThemeSettings
|
||||
@@ -31,6 +31,7 @@ interface ThreePaneLayoutProps {
|
||||
showSettings: boolean
|
||||
showExplore?: boolean
|
||||
showMe?: boolean
|
||||
showProfile?: boolean
|
||||
|
||||
// Bookmarks pane
|
||||
bookmarks: Bookmark[]
|
||||
@@ -89,6 +90,9 @@ interface ThreePaneLayoutProps {
|
||||
|
||||
// Optional Me content
|
||||
me?: React.ReactNode
|
||||
|
||||
// Optional Profile content
|
||||
profile?: React.ReactNode
|
||||
}
|
||||
|
||||
const ThreePaneLayout: React.FC<ThreePaneLayoutProps> = (props) => {
|
||||
@@ -221,8 +225,8 @@ const ThreePaneLayout: React.FC<ThreePaneLayoutProps> = (props) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Mobile bookmark button - only show when viewing article (not on settings/explore/me) */}
|
||||
{isMobile && !props.isSidebarOpen && props.isHighlightsCollapsed && !props.showSettings && !props.showExplore && !props.showMe && (
|
||||
{/* Mobile bookmark button - only show when viewing article (not on settings/explore/me/profile) */}
|
||||
{isMobile && !props.isSidebarOpen && props.isHighlightsCollapsed && !props.showSettings && !props.showExplore && !props.showMe && !props.showProfile && (
|
||||
<button
|
||||
className={`fixed z-[900] bg-zinc-800/70 border border-zinc-600/40 rounded-lg text-zinc-200 flex items-center justify-center transition-all duration-300 active:scale-95 backdrop-blur-sm md:hidden ${
|
||||
showMobileButtons ? 'opacity-90 visible' : 'opacity-0 invisible pointer-events-none'
|
||||
@@ -241,8 +245,8 @@ const ThreePaneLayout: React.FC<ThreePaneLayoutProps> = (props) => {
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Mobile highlights button - only show when viewing article (not on settings/explore/me) */}
|
||||
{isMobile && !props.isSidebarOpen && props.isHighlightsCollapsed && !props.showSettings && !props.showExplore && !props.showMe && (
|
||||
{/* Mobile highlights button - only show when viewing article (not on settings/explore/me/profile) */}
|
||||
{isMobile && !props.isSidebarOpen && props.isHighlightsCollapsed && !props.showSettings && !props.showExplore && !props.showMe && !props.showProfile && (
|
||||
<button
|
||||
className={`fixed z-[900] border border-zinc-600/40 rounded-lg flex items-center justify-center transition-all duration-300 active:scale-95 backdrop-blur-sm md:hidden ${
|
||||
showMobileButtons ? 'opacity-90 visible' : 'opacity-0 invisible pointer-events-none'
|
||||
@@ -320,6 +324,11 @@ const ThreePaneLayout: React.FC<ThreePaneLayoutProps> = (props) => {
|
||||
<>
|
||||
{props.me}
|
||||
</>
|
||||
) : props.showProfile && props.profile ? (
|
||||
// Render Profile inside the main pane to keep side panels
|
||||
<>
|
||||
{props.profile}
|
||||
</>
|
||||
) : (
|
||||
<ContentPanel
|
||||
loading={props.readerLoading}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { EventFactory } from 'applesauce-factory'
|
||||
import { AccountManager } from 'applesauce-accounts'
|
||||
import { UserSettings, loadSettings, saveSettings, watchSettings } from '../services/settingsService'
|
||||
import { loadFont, getFontFamily } from '../utils/fontLoader'
|
||||
import { applyTheme } from '../utils/theme'
|
||||
import { RELAYS } from '../config/relays'
|
||||
|
||||
interface UseSettingsParams {
|
||||
@@ -47,7 +48,14 @@ export function useSettings({ relayPool, eventStore, pubkey, accountManager }: U
|
||||
const root = document.documentElement.style
|
||||
const fontKey = settings.readingFont || 'system'
|
||||
|
||||
console.log('🎨 Applying settings styles:', { fontKey, fontSize: settings.fontSize })
|
||||
console.log('🎨 Applying settings styles:', { fontKey, fontSize: settings.fontSize, theme: settings.theme })
|
||||
|
||||
// Apply theme with color variants (defaults to 'system' if not set)
|
||||
applyTheme(
|
||||
settings.theme ?? 'system',
|
||||
settings.darkColorTheme ?? 'midnight',
|
||||
settings.lightColorTheme ?? 'sepia'
|
||||
)
|
||||
|
||||
// Load font first and wait for it to be ready
|
||||
if (fontKey !== 'system') {
|
||||
|
||||
@@ -47,6 +47,10 @@ export interface UserSettings {
|
||||
imageCacheSizeMB?: number // Maximum cache size in megabytes (default: 210MB)
|
||||
// Mobile settings
|
||||
autoCollapseSidebarOnMobile?: boolean // Auto-collapse sidebar on mobile (default: true)
|
||||
// Theme preference
|
||||
theme?: 'dark' | 'light' | 'system' // default: system
|
||||
darkColorTheme?: 'black' | 'midnight' | 'charcoal' // default: midnight
|
||||
lightColorTheme?: 'paper-white' | 'sepia' | 'ivory' // default: sepia
|
||||
}
|
||||
|
||||
export async function loadSettings(
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
/* Body - keep only app-specific overrides */
|
||||
body {
|
||||
background: var(--color-bg-subtle);
|
||||
color: var(--color-text);
|
||||
overscroll-behavior: none;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
overflow-x: hidden;
|
||||
@@ -22,7 +24,7 @@ body.mobile-sidebar-open {
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
padding: 2rem;
|
||||
color: rgb(212 212 216); /* zinc-300 */
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -4,10 +4,6 @@
|
||||
line-height: 1.5;
|
||||
font-weight: 400;
|
||||
|
||||
color-scheme: light dark;
|
||||
color: rgba(255, 255, 255, 0.87);
|
||||
background-color: #242424;
|
||||
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
@@ -47,10 +43,246 @@
|
||||
--safe-area-right: env(safe-area-inset-right, 0px);
|
||||
}
|
||||
|
||||
/* Dark theme (default) */
|
||||
:root.theme-dark {
|
||||
color-scheme: dark;
|
||||
|
||||
--color-bg: #18181b; /* zinc-900 */
|
||||
--color-bg-elevated: #27272a; /* zinc-800 */
|
||||
--color-bg-subtle: #1e1e1e; /* between zinc-800 and zinc-900 */
|
||||
--color-border: #3f3f46; /* zinc-700 */
|
||||
--color-border-subtle: #52525b; /* zinc-600 */
|
||||
--color-text: #e4e4e7; /* zinc-200 */
|
||||
--color-text-secondary: #a1a1aa; /* zinc-400 */
|
||||
--color-text-muted: #71717a; /* zinc-500 */
|
||||
--color-primary: #6366f1; /* indigo-500 */
|
||||
--color-primary-hover: #4f46e5; /* indigo-600 */
|
||||
}
|
||||
|
||||
/* Light theme */
|
||||
:root.theme-light {
|
||||
color-scheme: light;
|
||||
|
||||
--color-bg: #ffffff; /* white */
|
||||
--color-bg-elevated: #f5f5f5; /* gray-100 */
|
||||
--color-bg-subtle: #fafafa; /* gray-50 */
|
||||
--color-border: #e5e7eb; /* gray-200 */
|
||||
--color-border-subtle: #d1d5db; /* gray-300 */
|
||||
--color-text: #111827; /* gray-900 */
|
||||
--color-text-secondary: #374151; /* gray-700 */
|
||||
--color-text-muted: #6b7280; /* gray-500 */
|
||||
--color-primary: #4f46e5; /* indigo-600 */
|
||||
--color-primary-hover: #4338ca; /* indigo-700 */
|
||||
|
||||
/* Highlight colors for light theme - use same Tailwind colors */
|
||||
--highlight-color-mine: #fde047; /* yellow-300 */
|
||||
--highlight-color-friends: #f97316; /* orange-500 */
|
||||
--highlight-color-nostrverse: #9333ea; /* purple-600 */
|
||||
}
|
||||
|
||||
/* System theme - follow OS preference */
|
||||
:root.theme-system {
|
||||
color-scheme: light dark;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root.theme-system {
|
||||
--color-bg: #18181b;
|
||||
--color-bg-elevated: #27272a;
|
||||
--color-bg-subtle: #1e1e1e;
|
||||
--color-border: #3f3f46;
|
||||
--color-border-subtle: #52525b;
|
||||
--color-text: #e4e4e7;
|
||||
--color-text-secondary: #a1a1aa;
|
||||
--color-text-muted: #71717a;
|
||||
--color-primary: #6366f1;
|
||||
--color-primary-hover: #4f46e5;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
:root {
|
||||
color: #213547;
|
||||
background-color: #ffffff;
|
||||
:root.theme-system {
|
||||
--color-bg: #ffffff;
|
||||
--color-bg-elevated: #f5f5f5;
|
||||
--color-bg-subtle: #fafafa;
|
||||
--color-border: #e5e7eb;
|
||||
--color-border-subtle: #d1d5db;
|
||||
--color-text: #111827;
|
||||
--color-text-secondary: #374151;
|
||||
--color-text-muted: #6b7280;
|
||||
--color-primary: #4f46e5;
|
||||
--color-primary-hover: #4338ca;
|
||||
|
||||
/* Standard highlight colors */
|
||||
--highlight-color-mine: #fde047;
|
||||
--highlight-color-friends: #f97316;
|
||||
--highlight-color-nostrverse: #9333ea;
|
||||
}
|
||||
}
|
||||
|
||||
/* Dark Color Theme Variants */
|
||||
/* Midnight (default) - current zinc palette */
|
||||
:root.dark-midnight {
|
||||
--color-bg: #18181b; /* zinc-900 */
|
||||
--color-bg-elevated: #27272a; /* zinc-800 */
|
||||
--color-bg-subtle: #0f0f11; /* darker than zinc-900 */
|
||||
--color-border: #3f3f46; /* zinc-700 */
|
||||
--color-border-subtle: #52525b; /* zinc-600 */
|
||||
--color-text: #e4e4e7; /* zinc-200 */
|
||||
--color-text-secondary: #a1a1aa; /* zinc-400 */
|
||||
--color-text-muted: #71717a; /* zinc-500 */
|
||||
}
|
||||
|
||||
/* Black - true black for OLED */
|
||||
:root.dark-black {
|
||||
--color-bg: #000000; /* true black */
|
||||
--color-bg-elevated: #0a0a0a; /* very dark gray */
|
||||
--color-bg-subtle: #000000; /* true black for body */
|
||||
--color-border: #1a1a1a;
|
||||
--color-border-subtle: #2a2a2a;
|
||||
--color-text: #e4e4e7; /* zinc-200 */
|
||||
--color-text-secondary: #a1a1aa; /* zinc-400 */
|
||||
--color-text-muted: #71717a; /* zinc-500 */
|
||||
}
|
||||
|
||||
/* Charcoal - warmer, softer dark */
|
||||
:root.dark-charcoal {
|
||||
--color-bg: #1c1c1e; /* warmer dark */
|
||||
--color-bg-elevated: #2c2c2e;
|
||||
--color-bg-subtle: #16161a; /* darker charcoal */
|
||||
--color-border: #3a3a3c;
|
||||
--color-border-subtle: #48484a;
|
||||
--color-text: #e4e4e7; /* zinc-200 */
|
||||
--color-text-secondary: #a1a1aa; /* zinc-400 */
|
||||
--color-text-muted: #71717a; /* zinc-500 */
|
||||
}
|
||||
|
||||
/* Light Color Theme Variants */
|
||||
/* Paper White (default) - pure white */
|
||||
:root.light-paper-white {
|
||||
--color-bg: #ffffff; /* white */
|
||||
--color-bg-elevated: #f5f5f5; /* gray-100 */
|
||||
--color-bg-subtle: #fafafa; /* gray-50 */
|
||||
--color-border: #e5e7eb; /* gray-200 */
|
||||
--color-border-subtle: #d1d5db; /* gray-300 */
|
||||
|
||||
/* Standard highlight colors */
|
||||
--highlight-color-mine: #fde047;
|
||||
--highlight-color-friends: #f97316;
|
||||
--highlight-color-nostrverse: #9333ea;
|
||||
}
|
||||
|
||||
/* Sepia - warm, reading-friendly */
|
||||
:root.light-sepia {
|
||||
--color-bg: #f4f1ea; /* warm beige */
|
||||
--color-bg-elevated: #ebe6db; /* darker beige */
|
||||
--color-bg-subtle: #f9f6f0; /* lighter beige */
|
||||
--color-border: #d4cfc4; /* warm gray border */
|
||||
--color-border-subtle: #c4bfb4;
|
||||
--color-text: #2d2a24; /* warm dark brown */
|
||||
--color-text-secondary: #5d5a54;
|
||||
--color-text-muted: #8d8a84;
|
||||
|
||||
/* Standard highlight colors */
|
||||
--highlight-color-mine: #fde047; /* yellow-300 */
|
||||
--highlight-color-friends: #f97316; /* orange-500 */
|
||||
--highlight-color-nostrverse: #9333ea; /* purple-600 */
|
||||
}
|
||||
|
||||
/* Ivory - soft, creamy */
|
||||
:root.light-ivory {
|
||||
--color-bg: #fffff0; /* ivory */
|
||||
--color-bg-elevated: #faf8f0; /* cream */
|
||||
--color-bg-subtle: #fefef8;
|
||||
--color-border: #e8e6de;
|
||||
--color-border-subtle: #d8d6ce;
|
||||
--color-text: #1a1a18; /* near black with warm tint */
|
||||
--color-text-secondary: #4a4a48;
|
||||
--color-text-muted: #7a7a78;
|
||||
|
||||
/* Standard highlight colors */
|
||||
--highlight-color-mine: #fde047;
|
||||
--highlight-color-friends: #f97316;
|
||||
--highlight-color-nostrverse: #9333ea;
|
||||
}
|
||||
|
||||
/* System theme color variants */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root.theme-system.dark-midnight {
|
||||
--color-bg: #18181b;
|
||||
--color-bg-elevated: #27272a;
|
||||
--color-bg-subtle: #0f0f11;
|
||||
--color-border: #3f3f46;
|
||||
--color-border-subtle: #52525b;
|
||||
--color-text: #e4e4e7;
|
||||
--color-text-secondary: #a1a1aa;
|
||||
--color-text-muted: #71717a;
|
||||
}
|
||||
|
||||
:root.theme-system.dark-black {
|
||||
--color-bg: #000000;
|
||||
--color-bg-elevated: #0a0a0a;
|
||||
--color-bg-subtle: #000000;
|
||||
--color-border: #1a1a1a;
|
||||
--color-border-subtle: #2a2a2a;
|
||||
--color-text: #e4e4e7;
|
||||
--color-text-secondary: #a1a1aa;
|
||||
--color-text-muted: #71717a;
|
||||
}
|
||||
|
||||
:root.theme-system.dark-charcoal {
|
||||
--color-bg: #1c1c1e;
|
||||
--color-bg-elevated: #2c2c2e;
|
||||
--color-bg-subtle: #16161a;
|
||||
--color-border: #3a3a3c;
|
||||
--color-border-subtle: #48484a;
|
||||
--color-text: #e4e4e7;
|
||||
--color-text-secondary: #a1a1aa;
|
||||
--color-text-muted: #71717a;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
:root.theme-system.light-paper-white {
|
||||
--color-bg: #ffffff;
|
||||
--color-bg-elevated: #f5f5f5;
|
||||
--color-bg-subtle: #fafafa;
|
||||
--color-border: #e5e7eb;
|
||||
--color-border-subtle: #d1d5db;
|
||||
|
||||
--highlight-color-mine: #fde047;
|
||||
--highlight-color-friends: #f97316;
|
||||
--highlight-color-nostrverse: #9333ea;
|
||||
}
|
||||
|
||||
:root.theme-system.light-sepia {
|
||||
--color-bg: #f4f1ea;
|
||||
--color-bg-elevated: #ebe6db;
|
||||
--color-bg-subtle: #f9f6f0;
|
||||
--color-border: #d4cfc4;
|
||||
--color-border-subtle: #c4bfb4;
|
||||
--color-text: #2d2a24;
|
||||
--color-text-secondary: #5d5a54;
|
||||
--color-text-muted: #8d8a84;
|
||||
|
||||
--highlight-color-mine: #fde047;
|
||||
--highlight-color-friends: #f97316;
|
||||
--highlight-color-nostrverse: #9333ea;
|
||||
}
|
||||
|
||||
:root.theme-system.light-ivory {
|
||||
--color-bg: #fffff0;
|
||||
--color-bg-elevated: #faf8f0;
|
||||
--color-bg-subtle: #fefef8;
|
||||
--color-border: #e8e6de;
|
||||
--color-border-subtle: #d8d6ce;
|
||||
--color-text: #1a1a18;
|
||||
--color-text-secondary: #4a4a48;
|
||||
--color-text-muted: #7a7a78;
|
||||
|
||||
--highlight-color-mine: #fde047;
|
||||
--highlight-color-friends: #f97316;
|
||||
--highlight-color-nostrverse: #9333ea;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
/* Bookmark item and blog post cards */
|
||||
.bookmark-item { background: rgb(24 24 27); /* zinc-900 */ padding: 1.5rem; border-radius: 12px; transition: all 0.2s ease; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); }
|
||||
.bookmark-item { background: var(--color-bg); padding: 1.5rem; border-radius: 12px; transition: all 0.2s ease; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); }
|
||||
.bookmark-item:hover { transform: translateY(-2px); box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15); }
|
||||
.bookmark-item h3 { margin: 0 0 0.5rem 0; color: rgb(255 255 255); /* white */ font-size: 1.2rem; }
|
||||
.bookmark-url { color: rgb(99 102 241); /* indigo-500 */ text-decoration: none; display: block; margin-bottom: 0.5rem; word-break: break-all; background: none; border: none; padding: 0; font: inherit; cursor: pointer; text-align: left; width: 100%; }
|
||||
.bookmark-item h3 { margin: 0 0 0.5rem 0; color: var(--color-text); font-size: 1.2rem; }
|
||||
.bookmark-url { color: var(--color-primary); text-decoration: none; display: block; margin-bottom: 0.5rem; word-break: break-all; background: none; border: none; padding: 0; font: inherit; cursor: pointer; text-align: left; width: 100%; }
|
||||
.bookmark-url:hover { text-decoration: underline; }
|
||||
.bookmark-content { color: rgb(212 212 216); /* zinc-300 */ margin: 0.5rem 0; line-height: 1.4; word-wrap: break-word; overflow-wrap: break-word; word-break: break-word; }
|
||||
.bookmark-meta { color: rgb(161 161 170); /* zinc-400 */ font-size: 0.9rem; margin-top: 0.5rem; }
|
||||
.bookmark-content { color: var(--color-text); margin: 0.5rem 0; line-height: 1.4; word-wrap: break-word; overflow-wrap: break-word; word-break: break-word; }
|
||||
.bookmark-meta { color: var(--color-text-secondary); font-size: 0.9rem; margin-top: 0.5rem; }
|
||||
|
||||
.individual-bookmarks { margin: 1rem 0; }
|
||||
.individual-bookmarks h4 { margin: 0 0 1rem 0; font-size: 1rem; color: rgb(255 255 255); /* white */ }
|
||||
.individual-bookmarks h4 { margin: 0 0 1rem 0; font-size: 1rem; color: var(--color-text); }
|
||||
|
||||
.bookmarks-grid { display: flex; flex-direction: column; gap: 1rem; width: 100%; max-width: 100%; }
|
||||
.bookmarks-grid.bookmarks-compact { gap: 0.5rem; }
|
||||
@@ -19,74 +19,75 @@
|
||||
.bookmarks-grid.bookmarks-large { gap: 1rem; }
|
||||
}
|
||||
|
||||
.individual-bookmark { background: transparent; padding: 1rem; border-radius: 8px; transition: all 0.2s ease; border: 1px solid rgb(39 39 42); /* zinc-800 */ word-wrap: break-word; overflow-wrap: break-word; word-break: break-word; overflow: hidden; }
|
||||
.individual-bookmark:hover { border-color: rgb(63 63 70); /* zinc-700 */ background: rgb(39 39 42); /* zinc-800 */ }
|
||||
.individual-bookmark { background: transparent; padding: 1rem; border-radius: 8px; transition: all 0.2s ease; border: 1px solid var(--color-bg-elevated); word-wrap: break-word; overflow-wrap: break-word; word-break: break-word; overflow: hidden; }
|
||||
.individual-bookmark:hover { border-color: var(--color-border); background: var(--color-bg-elevated); }
|
||||
|
||||
/* Compact view */
|
||||
.individual-bookmark.compact { padding: 0.5rem 0.5rem; background: transparent; border: none; border-bottom: 1px solid rgb(39 39 42); /* zinc-800 */ border-radius: 0; box-shadow: none; width: 100%; max-width: 100%; overflow: hidden; }
|
||||
.individual-bookmark.compact:hover { background: rgb(39 39 42); /* zinc-800 */ border-bottom-color: rgb(63 63 70); /* zinc-700 */ transform: none; box-shadow: none; }
|
||||
.individual-bookmark.compact { padding: 0.5rem 0.5rem; background: transparent; border: none; border-bottom: 1px solid var(--color-bg-elevated); border-radius: 0; box-shadow: none; width: 100%; max-width: 100%; overflow: hidden; }
|
||||
.individual-bookmark.compact:hover { background: var(--color-bg-elevated); border-bottom-color: var(--color-border); transform: none; box-shadow: none; }
|
||||
.compact-row { display: flex; align-items: center; gap: 0.5rem; height: 28px; width: 100%; min-width: 0; overflow: hidden; }
|
||||
.compact-thumbnail { width: 24px; height: 24px; flex-shrink: 0; border-radius: 4px; overflow: hidden; background: rgb(39 39 42); /* zinc-800 */ display: flex; align-items: center; justify-content: center; }
|
||||
.compact-thumbnail { width: 24px; height: 24px; flex-shrink: 0; border-radius: 4px; overflow: hidden; background: var(--color-bg-elevated); display: flex; align-items: center; justify-content: center; }
|
||||
.compact-thumbnail img { width: 100%; height: 100%; object-fit: cover; }
|
||||
.compact-row.clickable { cursor: pointer; }
|
||||
.compact-row.clickable:active { opacity: 0.8; }
|
||||
.bookmark-type-compact { display: flex; align-items: center; gap: 0.25rem; color: rgb(99 102 241); /* indigo-500 */ font-size: 0.85rem; flex-shrink: 0; }
|
||||
.compact-text { flex: 1; min-width: 0; color: rgb(212 212 216); /* zinc-300 */ font-size: 0.85rem; line-height: 1.2; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.bookmark-date-compact { font-size: 0.7rem; color: rgb(113 113 122); /* zinc-500 */ flex-shrink: 0; white-space: nowrap; }
|
||||
.compact-read-btn { background: transparent; color: rgb(161 161 170); /* zinc-400 */ border: none; padding: 0; border-radius: 4px; cursor: pointer; font-size: 0.75rem; display: flex; align-items: center; justify-content: center; width: 24px; height: 22px; flex-shrink: 0; transition: color 0.2s ease; }
|
||||
.compact-read-btn:hover { color: rgb(212 212 216); /* zinc-300 */ }
|
||||
.bookmark-type-compact { display: flex; align-items: center; gap: 0.25rem; color: var(--color-primary); font-size: 0.85rem; flex-shrink: 0; }
|
||||
.compact-text { flex: 1; min-width: 0; color: var(--color-text); font-size: 0.85rem; line-height: 1.2; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.bookmark-date-compact { font-size: 0.7rem; color: var(--color-text-muted); flex-shrink: 0; white-space: nowrap; }
|
||||
.compact-read-btn { background: transparent; color: var(--color-text-secondary); border: none; padding: 0; border-radius: 4px; cursor: pointer; font-size: 0.75rem; display: flex; align-items: center; justify-content: center; width: 24px; height: 22px; flex-shrink: 0; transition: color 0.2s ease; }
|
||||
.compact-read-btn:hover { color: var(--color-text); }
|
||||
.compact-read-btn:active { transform: translateY(1px); }
|
||||
|
||||
.bookmark-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.75rem; flex-wrap: wrap; gap: 0.5rem; }
|
||||
.bookmark-type { color: rgb(99 102 241); /* indigo-500 */ font-size: 0.9rem; display: flex; align-items: center; gap: 0.35rem; }
|
||||
.bookmark-id { font-family: monospace; font-size: 0.8rem; color: rgb(161 161 170); /* zinc-400 */ background: rgb(24 24 27); /* zinc-900 */ padding: 0.25rem 0.5rem; border-radius: 4px; }
|
||||
.bookmark-date { font-size: 0.8rem; color: rgb(113 113 122); /* zinc-500 */ }
|
||||
.bookmark-date-link { font-size: 0.8rem; color: rgb(113 113 122); /* zinc-500 */ text-decoration: none; transition: color 0.2s ease; }
|
||||
.bookmark-date-link:hover { color: rgb(96 165 250); /* blue-400 */ text-decoration: underline; }
|
||||
.individual-bookmark .bookmark-content { margin: 0.75rem 0; color: rgb(212 212 216); /* zinc-300 */ line-height: 1.6; font-size: 0.9rem; word-wrap: break-word; overflow-wrap: break-word; word-break: break-word; }
|
||||
.expand-toggle { margin: 0.25rem 0; background: transparent; border: none; color: rgb(161 161 170); /* zinc-400 */ cursor: pointer; width: 100%; height: 22px; display: flex; align-items: center; justify-content: center; }
|
||||
.expand-toggle:hover { color: rgb(212 212 216); /* zinc-300 */ }
|
||||
.bookmark-type { color: var(--color-primary); font-size: 0.9rem; display: flex; align-items: center; gap: 0.35rem; }
|
||||
.bookmark-id { font-family: monospace; font-size: 0.8rem; color: var(--color-text-secondary); background: var(--color-bg); padding: 0.25rem 0.5rem; border-radius: 4px; }
|
||||
.bookmark-date { font-size: 0.8rem; color: var(--color-text-muted); }
|
||||
.bookmark-date-link { font-size: 0.8rem; color: var(--color-text-muted); text-decoration: none; transition: color 0.2s ease; }
|
||||
.bookmark-date-link:hover { color: var(--color-primary); text-decoration: underline; }
|
||||
.individual-bookmark .bookmark-content { margin: 0.75rem 0; color: var(--color-text); line-height: 1.6; font-size: 0.9rem; word-wrap: break-word; overflow-wrap: break-word; word-break: break-word; }
|
||||
.expand-toggle { margin: 0.25rem 0; background: transparent; border: none; color: var(--color-text-secondary); cursor: pointer; width: 100%; height: 22px; display: flex; align-items: center; justify-content: center; }
|
||||
.expand-toggle:hover { color: var(--color-text); }
|
||||
.bookmark-footer { display: flex; justify-content: space-between; align-items: center; margin-top: 0.75rem; gap: 0.75rem; }
|
||||
.bookmark-meta-minimal { font-size: 0.8rem; color: rgb(161 161 170); /* zinc-400 */ }
|
||||
.author-link-minimal { color: rgb(161 161 170); /* zinc-400 */ text-decoration: none; transition: color 0.2s ease; }
|
||||
.author-link-minimal:hover { color: rgb(212 212 216); /* zinc-300 */ }
|
||||
.read-now-button-minimal { background: rgb(99 102 241); /* indigo-500 */ color: white; border: none; padding: 0.5rem 1rem; border-radius: 6px; cursor: pointer; font-weight: 600; font-size: 0.85rem; transition: all 0.2s ease; white-space: nowrap; }
|
||||
.read-now-button-minimal:hover { background: rgb(79 70 229); /* indigo-600 */ }
|
||||
.expand-toggle-urls { margin-top: 0.5rem; background: transparent; border: none; color: rgb(99 102 241); /* indigo-500 */ cursor: pointer; font-size: 0.8rem; padding: 0.25rem 0; text-decoration: underline; }
|
||||
.expand-toggle-urls:hover { color: rgb(129 140 248); /* indigo-400 */ }
|
||||
.bookmark-meta-minimal { font-size: 0.8rem; color: var(--color-text-secondary); }
|
||||
.author-link-minimal { color: var(--color-text-secondary); text-decoration: none; transition: color 0.2s ease; }
|
||||
.author-link-minimal:hover { color: var(--color-text); }
|
||||
.read-now-button-minimal { background: var(--color-primary); color: white; border: none; padding: 0.5rem 1rem; border-radius: 6px; cursor: pointer; font-weight: 600; font-size: 0.85rem; transition: all 0.2s ease; white-space: nowrap; }
|
||||
.read-now-button-minimal:hover { background: var(--color-primary-hover); }
|
||||
.expand-toggle-urls { margin-top: 0.5rem; background: transparent; border: none; color: var(--color-primary); cursor: pointer; font-size: 0.8rem; padding: 0.25rem 0; text-decoration: underline; }
|
||||
.expand-toggle-urls:hover { color: var(--color-primary-hover); }
|
||||
|
||||
/* Large preview view */
|
||||
.individual-bookmark.large { padding: 0; display: flex; flex-direction: column; overflow: hidden; border: 1px solid rgb(39 39 42); /* zinc-800 */ }
|
||||
.large-preview-image { width: 100%; height: 180px; background: rgb(24 24 27); /* zinc-900 */ background-size: cover; background-position: center; background-repeat: no-repeat; display: flex; align-items: center; justify-content: center; cursor: pointer; transition: all 0.2s ease; border-bottom: 1px solid rgb(63 63 70); /* zinc-700 */ position: relative; }
|
||||
.individual-bookmark.large { padding: 0; display: flex; flex-direction: column; overflow: hidden; border: 1px solid var(--color-bg-elevated); }
|
||||
.large-preview-image { width: 100%; height: 180px; background: var(--color-bg); background-size: cover; background-position: center; background-repeat: no-repeat; display: flex; align-items: center; justify-content: center; cursor: pointer; transition: all 0.2s ease; border-bottom: 1px solid var(--color-border); position: relative; }
|
||||
.large-preview-image:hover { opacity: 0.9; }
|
||||
.large-preview-image::after { content: ''; position: absolute; inset: 0; background: linear-gradient(to bottom, transparent 60%, rgba(0,0,0,0.3) 100%); pointer-events: none; }
|
||||
.preview-placeholder { font-size: 3rem; color: rgb(82 82 91); /* zinc-600 */ }
|
||||
.preview-placeholder { font-size: 3rem; color: var(--color-border-subtle); }
|
||||
.large-content { padding: 1.25rem; }
|
||||
.large-text { color: rgb(212 212 216); /* zinc-300 */ font-size: 0.95rem; line-height: 1.6; margin-bottom: 1rem; display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; }
|
||||
.large-footer { display: flex; align-items: center; gap: 1rem; flex-wrap: wrap; font-size: 0.8rem; color: rgb(161 161 170); /* zinc-400 */ padding-top: 0.75rem; border-top: 1px solid rgb(63 63 70); /* zinc-700 */ }
|
||||
.large-text { color: var(--color-text); font-size: 0.95rem; line-height: 1.6; margin-bottom: 1rem; display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; }
|
||||
.large-footer { display: flex; align-items: center; gap: 1rem; flex-wrap: wrap; font-size: 0.8rem; color: var(--color-text-secondary); padding-top: 0.75rem; border-top: 1px solid var(--color-border); }
|
||||
.large-author { flex: 1; }
|
||||
.large-read-button { background: rgb(99 102 241); /* indigo-500 */ color: white; border: none; padding: 0.5rem 1rem; border-radius: 6px; cursor: pointer; font-weight: 600; font-size: 0.85rem; transition: all 0.2s ease; display: flex; align-items: center; gap: 0.5rem; }
|
||||
.large-read-button:hover { background: rgb(79 70 229); /* indigo-600 */ }
|
||||
.large-read-button { background: var(--color-primary); color: white; border: none; padding: 0.5rem 1rem; border-radius: 6px; cursor: pointer; font-weight: 600; font-size: 0.85rem; transition: all 0.2s ease; display: flex; align-items: center; gap: 0.5rem; }
|
||||
.large-read-button:hover { background: var(--color-primary-hover); }
|
||||
|
||||
/* Blog cards (Explore) */
|
||||
.explore-container { padding: 2rem; max-width: 1400px; margin: 0 auto; min-height: 100vh; }
|
||||
.explore-header { text-align: center; margin-bottom: 3rem; }
|
||||
.explore-header h1 { font-size: 2.5rem; margin: 0 0 1rem 0; color: rgb(99 102 241); /* indigo-500 */ display: flex; align-items: center; justify-content: center; gap: 1rem; }
|
||||
.explore-subtitle { font-size: 1.125rem; color: rgba(255, 255, 255, 0.7); margin: 0; }
|
||||
.explore-loading, .explore-error { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 1rem; color: rgba(255, 255, 255, 0.7); }
|
||||
.explore-header h1 { font-size: 2.5rem; margin: 0 0 1rem 0; color: var(--color-primary); display: flex; align-items: center; justify-content: center; gap: 1rem; }
|
||||
.explore-subtitle { font-size: 1.125rem; color: var(--color-text-secondary); margin: 0; }
|
||||
.explore-loading, .explore-error, .explore-empty { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 1rem; color: var(--color-text-secondary); }
|
||||
.explore-loading { min-height: 0; padding: 0.25rem 0; }
|
||||
.explore-error { color: rgb(239 68 68); /* red-500 */ }
|
||||
.explore-empty { color: var(--color-text-secondary); }
|
||||
.explore-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); gap: 2rem; margin-top: 2rem; }
|
||||
.blog-post-card { background: rgb(24 24 27); /* zinc-900 */ border: 1px solid rgb(63 63 70); /* zinc-700 */ border-radius: 12px; overflow: hidden; transition: all 0.3s ease; cursor: pointer; display: flex; flex-direction: column; height: 100%; }
|
||||
.blog-post-card:hover { border-color: rgb(99 102 241); /* indigo-500 */ transform: translateY(-4px); box-shadow: 0 8px 24px rgba(99, 102, 241, 0.15); }
|
||||
.blog-post-card-image { width: 100%; height: 200px; overflow: hidden; background: rgb(9 9 11); /* zinc-950 */ display: flex; align-items: center; justify-content: center; }
|
||||
.blog-post-card { background: var(--color-bg); border: 1px solid var(--color-border); border-radius: 12px; overflow: hidden; transition: all 0.3s ease; cursor: pointer; display: flex; flex-direction: column; height: 100%; }
|
||||
.blog-post-card:hover { border-color: var(--color-primary); transform: translateY(-4px); box-shadow: 0 8px 24px rgba(99, 102, 241, 0.15); }
|
||||
.blog-post-card-image { width: 100%; height: 200px; overflow: hidden; background: var(--color-bg-subtle); display: flex; align-items: center; justify-content: center; }
|
||||
.blog-post-card-image img { width: 100%; height: 100%; object-fit: cover; transition: transform 0.3s ease; }
|
||||
.blog-post-card:hover .blog-post-card-image img { transform: scale(1.05); }
|
||||
.blog-post-image-placeholder { font-size: 3rem; color: rgb(82 82 91); /* zinc-600 */ display: flex; align-items: center; justify-content: center; }
|
||||
.blog-post-image-placeholder { font-size: 3rem; color: var(--color-border-subtle); display: flex; align-items: center; justify-content: center; }
|
||||
.blog-post-card-content { padding: 1.5rem; display: flex; flex-direction: column; gap: 1rem; flex: 1; }
|
||||
.blog-post-card-title { font-size: 1.25rem; font-weight: 600; margin: 0; color: rgba(255, 255, 255, 0.95); line-height: 1.4; overflow: hidden; text-overflow: ellipsis; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; }
|
||||
.blog-post-card-summary { font-size: 0.875rem; color: rgba(255, 255, 255, 0.6); margin: 0; line-height: 1.6; overflow: hidden; text-overflow: ellipsis; display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; flex: 1; }
|
||||
.blog-post-card-meta { display: flex; align-items: center; justify-content: space-between; gap: 1rem; padding-top: 0.75rem; border-top: 1px solid rgb(63 63 70); /* zinc-700 */ font-size: 0.75rem; color: rgba(255, 255, 255, 0.5); flex-wrap: wrap; }
|
||||
.blog-post-card-title { font-size: 1.25rem; font-weight: 600; margin: 0; color: var(--color-text); line-height: 1.4; overflow: hidden; text-overflow: ellipsis; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; }
|
||||
.blog-post-card-summary { font-size: 0.875rem; color: var(--color-text-secondary); margin: 0; line-height: 1.6; overflow: hidden; text-overflow: ellipsis; display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; flex: 1; }
|
||||
.blog-post-card-meta { display: flex; align-items: center; justify-content: space-between; gap: 1rem; padding-top: 0.75rem; border-top: 1px solid var(--color-border); font-size: 0.75rem; color: var(--color-text-muted); flex-wrap: wrap; }
|
||||
.blog-post-card-author, .blog-post-card-date { display: flex; align-items: center; gap: 0.5rem; }
|
||||
.blog-post-card-author svg, .blog-post-card-date svg { opacity: 0.7; }
|
||||
@media (max-width: 768px) {
|
||||
@@ -97,4 +98,3 @@
|
||||
.blog-post-card-content { padding: 1rem; }
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -4,28 +4,28 @@
|
||||
.setting-label { text-align: left; flex: 1; }
|
||||
.setting-control { display: flex; justify-content: flex-end; align-items: center; }
|
||||
.setting-group.setting-inline label { margin-bottom: 0; }
|
||||
.setting-group label { display: block; margin-bottom: 0.5rem; color: rgb(212 212 216); /* zinc-300 */ font-weight: 500; text-align: left; }
|
||||
.setting-group label { display: block; margin-bottom: 0.5rem; color: var(--color-text); font-weight: 500; text-align: left; }
|
||||
.setting-buttons { display: flex; align-items: center; gap: 0.5rem; }
|
||||
.color-picker { display: flex; align-items: center; gap: 0.5rem; }
|
||||
.color-swatch { width: 33px; height: 33px; border: 1px solid rgb(82 82 91); /* zinc-600 */ border-radius: 6px; cursor: pointer; transition: all 0.2s; position: relative; }
|
||||
.color-swatch:hover { border-color: rgb(161 161 170); /* zinc-400 */ }
|
||||
.color-swatch.active { border-color: rgb(99 102 241); /* indigo-500 */ box-shadow: 0 0 0 2px rgb(99 102 241); /* indigo-500 */ }
|
||||
.color-swatch.active::after { content: '✓'; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); color: rgb(0 0 0); /* black */ font-size: 0.875rem; font-weight: bold; text-shadow: 0 0 2px rgb(255 255 255); /* white */ }
|
||||
.font-size-btn { min-width: 33px; height: 33px; padding: 0; background: transparent; border: 1px solid rgb(82 82 91); /* zinc-600 */ border-radius: 6px; color: rgb(212 212 216); /* zinc-300 */ cursor: pointer; transition: all 0.2s; font-weight: bold; display: flex; align-items: center; justify-content: center; }
|
||||
.font-size-btn:hover { background: rgb(63 63 70); /* zinc-700 */ border-color: rgb(113 113 122); /* zinc-500 */ }
|
||||
.font-size-btn.active { background: rgb(99 102 241); /* indigo-500 */ border-color: rgb(99 102 241); /* indigo-500 */ color: white; }
|
||||
.color-swatch { width: 33px; height: 33px; border: 1px solid var(--color-border-subtle); border-radius: 6px; cursor: pointer; transition: all 0.2s; position: relative; }
|
||||
.color-swatch:hover { border-color: var(--color-text-secondary); }
|
||||
.color-swatch.active { border-color: var(--color-primary); box-shadow: 0 0 0 2px var(--color-primary); }
|
||||
.color-swatch.active::after { content: '✓'; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); color: rgb(0 0 0); font-size: 0.875rem; font-weight: bold; text-shadow: 0 0 2px rgb(255 255 255); }
|
||||
.font-size-btn { min-width: 33px; height: 33px; padding: 0; background: transparent; border: 1px solid var(--color-border-subtle); border-radius: 6px; color: var(--color-text); cursor: pointer; transition: all 0.2s; font-weight: bold; display: flex; align-items: center; justify-content: center; }
|
||||
.font-size-btn:hover { background: var(--color-border); border-color: var(--color-text-muted); }
|
||||
.font-size-btn.active { background: var(--color-primary); border-color: var(--color-primary); color: white; }
|
||||
.setting-preview {
|
||||
margin: 1.5rem 0;
|
||||
padding: 1rem;
|
||||
background: rgb(24 24 27); /* zinc-900 */
|
||||
border: 1px solid rgb(63 63 70); /* zinc-700 */
|
||||
background: var(--color-bg);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 8px;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
.preview-label { font-size: 0.875rem; color: rgb(161 161 170); /* zinc-400 */ margin-bottom: 0.75rem; font-weight: 500; text-transform: uppercase; letter-spacing: 0.05em; }
|
||||
.preview-label { font-size: 0.875rem; color: var(--color-text-secondary); margin-bottom: 0.75rem; font-weight: 500; text-transform: uppercase; letter-spacing: 0.05em; }
|
||||
.preview-content {
|
||||
color: rgb(228 228 231); /* zinc-200 */
|
||||
color: var(--color-text);
|
||||
line-height: 1.7;
|
||||
max-width: 100%;
|
||||
overflow-wrap: break-word;
|
||||
@@ -35,20 +35,20 @@
|
||||
.preview-content h3 {
|
||||
margin: 0 0 1rem 0;
|
||||
font-size: 1.5em;
|
||||
color: rgb(255 255 255); /* white */
|
||||
color: var(--color-text);
|
||||
word-wrap: break-word;
|
||||
}
|
||||
.preview-content p {
|
||||
margin: 0.75rem 0;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
.setting-select { width: 100%; padding: 0.5rem; background: rgb(39 39 42); /* zinc-800 */ border: 1px solid rgb(82 82 91); /* zinc-600 */ border-radius: 4px; color: rgb(255 255 255); /* white */ font-size: 1rem; }
|
||||
.setting-select { width: 100%; padding: 0.5rem; background: var(--color-bg-elevated); border: 1px solid var(--color-border-subtle); border-radius: 4px; color: var(--color-text); font-size: 1rem; }
|
||||
.setting-inline .setting-select { width: auto; min-width: 200px; flex: 1; }
|
||||
.setting-select:focus { outline: none; border-color: rgb(99 102 241); /* indigo-500 */ }
|
||||
.setting-select:focus { outline: none; border-color: var(--color-primary); }
|
||||
.font-select option { padding: 0.5rem; font-size: 1rem; }
|
||||
.checkbox-label { display: flex !important; align-items: center; gap: 0.75rem; cursor: pointer; user-select: none; text-align: left; justify-content: flex-start; margin-bottom: 0 !important; font-weight: normal !important; }
|
||||
.setting-checkbox { width: 18px; height: 18px; cursor: pointer; flex-shrink: 0; margin: 0; accent-color: rgb(99 102 241); /* indigo-500 */ }
|
||||
.checkbox-label span { color: rgb(228 228 231); /* zinc-200 */ text-align: left; font-weight: 500; }
|
||||
.setting-checkbox { width: 18px; height: 18px; cursor: pointer; flex-shrink: 0; margin: 0; accent-color: var(--color-primary); }
|
||||
.checkbox-label span { color: var(--color-text); text-align: left; font-weight: 500; }
|
||||
|
||||
/* Mobile responsive styles */
|
||||
@media (max-width: 768px) {
|
||||
@@ -81,4 +81,3 @@
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -3,10 +3,10 @@
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 1px solid rgb(82 82 91); /* zinc-600 */
|
||||
border: 1px solid var(--color-border-subtle);
|
||||
border-radius: 6px;
|
||||
background: rgb(39 39 42); /* zinc-800 */
|
||||
color: rgb(228 228 231); /* zinc-200 */
|
||||
background: var(--color-bg-elevated);
|
||||
color: var(--color-text);
|
||||
cursor: pointer;
|
||||
min-width: 33px;
|
||||
min-height: 33px;
|
||||
@@ -14,16 +14,16 @@
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.icon-button:hover { background: rgb(63 63 70); /* zinc-700 */ }
|
||||
.icon-button:hover { background: var(--color-border); }
|
||||
.icon-button:active { transform: translateY(1px); }
|
||||
|
||||
.icon-button.primary { background: rgb(99 102 241); /* indigo-500 */ color: white; border-color: rgb(99 102 241); /* indigo-500 */ }
|
||||
.icon-button.primary { background: var(--color-primary); color: white; border-color: var(--color-primary); }
|
||||
.icon-button.primary:hover { filter: brightness(1.05); }
|
||||
|
||||
.icon-button.success { background: rgb(99 102 241); /* indigo-500 */ color: white; border-color: rgb(99 102 241); /* indigo-500 */ }
|
||||
.icon-button.success { background: var(--color-primary); color: white; border-color: var(--color-primary); }
|
||||
.icon-button.success:hover { filter: brightness(1.1); }
|
||||
|
||||
.icon-button.ghost { background: rgb(39 39 42); /* zinc-800 */ }
|
||||
.icon-button.ghost { background: var(--color-bg-elevated); }
|
||||
|
||||
/* Mobile touch target improvements */
|
||||
@media (max-width: 768px) {
|
||||
@@ -42,9 +42,8 @@
|
||||
|
||||
/* Disable hover effects on touch devices */
|
||||
@media (pointer: coarse) {
|
||||
.icon-button:hover { background: rgb(39 39 42); /* zinc-800 */ }
|
||||
.icon-button.ghost:hover { background: rgb(39 39 42); /* zinc-800 */ }
|
||||
.icon-button:active { background: rgb(63 63 70); /* zinc-700 */ }
|
||||
.icon-button:hover { background: var(--color-bg-elevated); }
|
||||
.icon-button.ghost:hover { background: var(--color-bg-elevated); }
|
||||
.icon-button:active { background: var(--color-border); }
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
margin-top: 1rem;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
overflow-x: auto;
|
||||
max-width: 600px;
|
||||
margin-left: auto;
|
||||
@@ -18,7 +18,7 @@
|
||||
background: none;
|
||||
border: none;
|
||||
border-bottom: 2px solid transparent;
|
||||
color: var(--text-secondary, rgb(161 161 170)); /* zinc-400 */
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 0.95rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
@@ -28,25 +28,32 @@
|
||||
}
|
||||
|
||||
.me-tab:hover {
|
||||
color: var(--text-primary, rgb(228 228 231)); /* zinc-200 */
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
color: var(--color-text);
|
||||
background: var(--color-bg-elevated);
|
||||
}
|
||||
|
||||
.me-tab.active {
|
||||
color: var(--primary-color, rgb(139 92 246)); /* purple-500 */
|
||||
border-bottom-color: var(--primary-color, rgb(139 92 246)); /* purple-500 */
|
||||
color: var(--color-primary);
|
||||
border-bottom-color: var(--color-primary);
|
||||
}
|
||||
|
||||
/* Highlights tab uses the user's custom "my highlights" color */
|
||||
/* Highlights tab uses the actual highlight style when active */
|
||||
.me-tab[data-tab="highlights"].active {
|
||||
color: var(--highlight-color-mine, rgb(253 224 71)); /* yellow-300 */
|
||||
color: var(--color-text);
|
||||
border-bottom-color: var(--highlight-color-mine, rgb(253 224 71)); /* yellow-300 */
|
||||
background: color-mix(in srgb, var(--highlight-color-mine, rgb(253 224 71)) 35%, transparent);
|
||||
box-shadow: 0 0 8px color-mix(in srgb, var(--highlight-color-mine, rgb(253 224 71)) 20%, transparent);
|
||||
}
|
||||
|
||||
.me-tab[data-tab="highlights"].active:hover {
|
||||
background: color-mix(in srgb, var(--highlight-color-mine, rgb(253 224 71)) 50%, transparent);
|
||||
box-shadow: 0 0 12px color-mix(in srgb, var(--highlight-color-mine, rgb(253 224 71)) 30%, transparent);
|
||||
}
|
||||
|
||||
/* Reading List tab uses blue color to match bookmarks icon */
|
||||
.me-tab[data-tab="reading-list"].active {
|
||||
color: rgb(99 102 241); /* indigo-500 */
|
||||
border-bottom-color: rgb(99 102 241); /* indigo-500 */
|
||||
color: var(--color-primary);
|
||||
border-bottom-color: var(--color-primary);
|
||||
}
|
||||
|
||||
.me-tab svg {
|
||||
@@ -80,25 +87,25 @@
|
||||
|
||||
/* Enhanced border styling for reading list cards */
|
||||
.bookmarks-list .individual-bookmark {
|
||||
border: 1px solid rgb(82 82 91) !important; /* zinc-600 */
|
||||
background: rgb(24 24 27) !important; /* zinc-900 */
|
||||
border: 1px solid var(--color-border-subtle) !important;
|
||||
background: var(--color-bg) !important;
|
||||
}
|
||||
|
||||
.bookmarks-list .individual-bookmark:hover {
|
||||
border-color: rgb(113 113 122) !important; /* zinc-500 */
|
||||
background: rgb(39 39 42) !important; /* zinc-800 */
|
||||
border-color: var(--color-border) !important;
|
||||
background: var(--color-bg-elevated) !important;
|
||||
}
|
||||
|
||||
.bookmark-item {
|
||||
padding: 1rem;
|
||||
background: var(--card-bg, rgb(255 255 255)); /* white */
|
||||
border: 1px solid var(--border-color, rgb(228 228 231)); /* zinc-200 */
|
||||
background: var(--color-bg);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 8px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.bookmark-item:hover {
|
||||
border-color: var(--primary-color, rgb(139 92 246)); /* purple-500 */
|
||||
border-color: var(--color-primary);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
@@ -110,13 +117,13 @@
|
||||
.bookmark-item h3 {
|
||||
margin: 0 0 0.5rem 0;
|
||||
font-size: 1.1rem;
|
||||
color: var(--text-primary, rgb(0 0 0)); /* black */
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.bookmark-item p {
|
||||
margin: 0;
|
||||
font-size: 0.9rem;
|
||||
color: var(--text-secondary, rgb(113 113 122)); /* zinc-500 */
|
||||
color: var(--color-text-secondary);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,28 +1,27 @@
|
||||
/* Add Bookmark Modal */
|
||||
.modal-overlay { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0, 0, 0, 0.75); display: flex; align-items: center; justify-content: center; z-index: 10000; padding: 1rem; }
|
||||
.modal-content { background: rgb(24 24 27); /* zinc-900 */ border: 1px solid rgb(63 63 70); /* zinc-700 */ border-radius: 12px; max-width: 500px; width: 100%; max-height: 90vh; overflow-y: auto; box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5); box-sizing: border-box; }
|
||||
.modal-content { background: var(--color-bg); border: 1px solid var(--color-border); border-radius: 12px; max-width: 500px; width: 100%; max-height: 90vh; overflow-y: auto; box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5); box-sizing: border-box; }
|
||||
@media (max-width: 768px) {
|
||||
.modal-overlay { padding: 0; align-items: flex-end; }
|
||||
.modal-content { max-width: 100%; max-height: 95vh; max-height: 95dvh; border-radius: 16px 16px 0 0; margin: 0; padding-bottom: var(--safe-area-bottom); }
|
||||
}
|
||||
.modal-header { display: flex; align-items: center; justify-content: space-between; padding: 1.5rem; border-bottom: 1px solid rgb(63 63 70); /* zinc-700 */ }
|
||||
.modal-header h2 { margin: 0; font-size: 1.5rem; color: rgb(255 255 255); /* white */ }
|
||||
.modal-header { display: flex; align-items: center; justify-content: space-between; padding: 1.5rem; border-bottom: 1px solid var(--color-border); }
|
||||
.modal-header h2 { margin: 0; font-size: 1.5rem; color: var(--color-text); }
|
||||
.modal-form { padding: 1.5rem; }
|
||||
.form-group { margin-bottom: 1.25rem; }
|
||||
.form-group label { display: flex; align-items: center; justify-content: space-between; margin-bottom: 0.5rem; color: rgb(212 212 216); /* zinc-300 */ font-size: 0.9rem; font-weight: 500; }
|
||||
.fetching-indicator { font-size: 0.8rem; color: rgb(161 161 170); /* zinc-400 */ font-weight: normal; display: inline-flex; align-items: center; gap: 0.5rem; }
|
||||
.form-group input, .form-group textarea { width: 100%; padding: 0.75rem; background: rgb(39 39 42); /* zinc-800 */ border: 1px solid rgb(82 82 91); /* zinc-600 */ border-radius: 6px; color: rgb(255 255 255); /* white */ font-size: 1rem; font-family: inherit; transition: border-color 0.2s; box-sizing: border-box; }
|
||||
.form-group input:focus, .form-group textarea:focus { outline: none; border-color: rgb(99 102 241); /* indigo-500 */ }
|
||||
.form-group label { display: flex; align-items: center; justify-content: space-between; margin-bottom: 0.5rem; color: var(--color-text); font-size: 0.9rem; font-weight: 500; }
|
||||
.fetching-indicator { font-size: 0.8rem; color: var(--color-text-secondary); font-weight: normal; display: inline-flex; align-items: center; gap: 0.5rem; }
|
||||
.form-group input, .form-group textarea { width: 100%; padding: 0.75rem; background: var(--color-bg-elevated); border: 1px solid var(--color-border-subtle); border-radius: 6px; color: var(--color-text); font-size: 1rem; font-family: inherit; transition: border-color 0.2s; box-sizing: border-box; }
|
||||
.form-group input:focus, .form-group textarea:focus { outline: none; border-color: var(--color-primary); }
|
||||
.form-group input:disabled, .form-group textarea:disabled { opacity: 0.6; cursor: not-allowed; }
|
||||
.form-group textarea { resize: vertical; min-height: 80px; }
|
||||
.form-helper-text { margin-top: 0.25rem; font-size: 0.8rem; color: rgb(161 161 170); /* zinc-400 */ line-height: 1.4; }
|
||||
.modal-error { padding: 0.75rem; background: rgba(220, 53, 69, 0.1); border: 1px solid rgb(220 38 38); /* red-600 */ border-radius: 6px; color: rgb(220 38 38); /* red-600 */ font-size: 0.9rem; margin-bottom: 1rem; }
|
||||
.form-helper-text { margin-top: 0.25rem; font-size: 0.8rem; color: var(--color-text-secondary); line-height: 1.4; }
|
||||
.modal-error { padding: 0.75rem; background: rgba(220, 53, 69, 0.1); border: 1px solid rgb(220 38 38); border-radius: 6px; color: rgb(220 38 38); font-size: 0.9rem; margin-bottom: 1rem; }
|
||||
.modal-actions { display: flex; gap: 0.75rem; justify-content: flex-end; margin-top: 1.5rem; }
|
||||
.btn-secondary { padding: 0.75rem 1.5rem; background: rgb(39 39 42); /* zinc-800 */ border: 1px solid rgb(82 82 91); /* zinc-600 */ border-radius: 6px; color: rgb(212 212 216); /* zinc-300 */ font-size: 1rem; cursor: pointer; transition: all 0.2s; }
|
||||
.btn-secondary:hover:not(:disabled) { background: rgb(63 63 70); /* zinc-700 */ border-color: rgb(99 102 241); /* indigo-500 */ color: white; }
|
||||
.btn-secondary { padding: 0.75rem 1.5rem; background: var(--color-bg-elevated); border: 1px solid var(--color-border-subtle); border-radius: 6px; color: var(--color-text); font-size: 1rem; cursor: pointer; transition: all 0.2s; }
|
||||
.btn-secondary:hover:not(:disabled) { background: var(--color-border); border-color: var(--color-primary); color: var(--color-text); }
|
||||
.btn-secondary:disabled { opacity: 0.6; cursor: not-allowed; }
|
||||
.btn-primary { padding: 0.75rem 1.5rem; background: rgb(99 102 241); /* indigo-500 */ border: none; border-radius: 6px; color: white; font-size: 1rem; cursor: pointer; transition: background-color 0.2s; }
|
||||
.btn-primary:hover:not(:disabled) { background: rgb(79 70 229); /* indigo-600 */ }
|
||||
.btn-primary { padding: 0.75rem 1.5rem; background: var(--color-primary); border: none; border-radius: 6px; color: white; font-size: 1rem; cursor: pointer; transition: background-color 0.2s; }
|
||||
.btn-primary:hover:not(:disabled) { background: var(--color-primary-hover); }
|
||||
.btn-primary:disabled { opacity: 0.6; cursor: not-allowed; }
|
||||
|
||||
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
/* Profile UI fragments */
|
||||
.author-card-container { display: flex; justify-content: center; padding: 2rem 1rem; }
|
||||
.author-card { display: flex; gap: 1rem; padding: 1.5rem; background: rgb(24 24 27); /* zinc-900 */ border: 1px solid rgb(63 63 70); /* zinc-700 */ border-radius: 12px; max-width: 600px; width: 100%; }
|
||||
.author-card-avatar { flex-shrink: 0; width: 60px; height: 60px; border-radius: 50%; overflow: hidden; background: rgb(39 39 42); /* zinc-800 */ display: flex; align-items: center; justify-content: center; color: rgb(113 113 122); /* zinc-500 */ }
|
||||
.author-card { display: flex; gap: 1rem; padding: 1.5rem; background: var(--color-bg); border: 1px solid var(--color-border); border-radius: 12px; max-width: 600px; width: 100%; transition: all 0.2s ease; }
|
||||
.author-card-clickable:hover { border-color: var(--color-primary); background: var(--color-bg-elevated); transform: translateY(-1px); }
|
||||
.author-card-clickable:active { transform: translateY(0); }
|
||||
.author-card-avatar { flex-shrink: 0; width: 60px; height: 60px; border-radius: 50%; overflow: hidden; background: var(--color-bg-elevated); display: flex; align-items: center; justify-content: center; color: var(--color-text-muted); }
|
||||
.author-card-avatar img { width: 100%; height: 100%; object-fit: cover; }
|
||||
.author-card-avatar svg { font-size: 2.5rem; }
|
||||
.author-card-content { flex: 1; min-width: 0; text-align: left; }
|
||||
.author-card-name { font-size: 1rem; font-weight: 600; color: rgb(228 228 231); /* zinc-200 */ margin-bottom: 0.5rem; text-align: left; }
|
||||
.author-card-bio { font-size: 0.9rem; color: rgb(161 161 170); /* zinc-400 */ line-height: 1.5; margin: 0; display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; text-overflow: ellipsis; text-align: left; }
|
||||
.author-card-name { font-size: 1rem; font-weight: 600; color: var(--color-text); margin-bottom: 0.5rem; text-align: left; }
|
||||
.author-card-bio { font-size: 0.9rem; color: var(--color-text-secondary); line-height: 1.5; margin: 0; display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; text-overflow: ellipsis; text-align: left; }
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.author-card-container {
|
||||
@@ -26,4 +28,3 @@
|
||||
.author-card-bio { font-size: 0.85rem; -webkit-line-clamp: 2; }
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -21,21 +21,21 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: var(--background-secondary);
|
||||
background: var(--color-bg-elevated);
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
transition: transform 0.3s ease;
|
||||
font-size: 1rem;
|
||||
color: var(--text-secondary);
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.pull-to-refresh-text {
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-secondary);
|
||||
color: var(--color-text-secondary);
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
font-weight: 500;
|
||||
background: var(--background-secondary);
|
||||
background: var(--color-bg-elevated);
|
||||
padding: 0.25rem 0.75rem;
|
||||
border-radius: 1rem;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/* Reader view */
|
||||
.reader {
|
||||
background: rgb(24 24 27); /* zinc-900 */
|
||||
border: 1px solid rgb(63 63 70); /* zinc-700 */
|
||||
background: var(--color-bg);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 8px;
|
||||
padding: 0.75rem;
|
||||
text-align: left;
|
||||
@@ -21,22 +21,22 @@
|
||||
margin: 0 -0.75rem 1rem -0.75rem; /* Negative margins to counteract reader padding */
|
||||
background: rgb(0 0 0); /* black */
|
||||
}
|
||||
.reader.empty { color: rgb(161 161 170); /* zinc-400 */ }
|
||||
.loading-spinner { display: flex; align-items: center; gap: 0.5rem; color: rgb(161 161 170); /* zinc-400 */ }
|
||||
.reader.empty { color: var(--color-text-secondary); }
|
||||
.loading-spinner { display: flex; align-items: center; gap: 0.5rem; color: var(--color-text-secondary); }
|
||||
.loading-spinner svg { font-size: 1.2rem; }
|
||||
.reader-header { margin-bottom: 2rem; position: relative; }
|
||||
.reader-title { margin: 0 0 0.75rem 0; font-family: var(--reading-font); font-size: 2.5rem; font-weight: 700; line-height: 1.2; }
|
||||
.reader-summary { color: rgb(212 212 216); /* zinc-300 */ font-size: 1.2rem; line-height: 1.6; margin: 0 0 1rem 0; font-family: var(--reading-font); }
|
||||
.reader-summary { color: var(--color-text); font-size: 1.2rem; line-height: 1.6; margin: 0 0 1rem 0; font-family: var(--reading-font); }
|
||||
.reader-meta { display: flex; align-items: center; gap: 0.75rem; flex-wrap: wrap; }
|
||||
.publish-date { display: flex; align-items: center; gap: 0.4rem; font-size: 0.813rem; color: rgba(161, 161, 170, 0.7); /* zinc-400 */ opacity: 0.85; }
|
||||
.publish-date { display: flex; align-items: center; gap: 0.4rem; font-size: 0.813rem; color: var(--color-text-muted); opacity: 0.85; }
|
||||
.publish-date svg { font-size: 0.75rem; opacity: 0.6; }
|
||||
.publish-date-topright { position: absolute; top: 1rem; right: 1rem; font-size: 0.813rem; color: rgb(255 255 255); /* white */ padding: 0.4rem 0.75rem; text-shadow: 0 2px 4px rgba(0, 0, 0, 0.5); z-index: 10; }
|
||||
.reading-time { display: flex; align-items: center; gap: 0.5rem; padding: 0.375rem 0.75rem; background: rgba(161, 161, 170, 0.1); /* zinc-400 */ border: 1px solid rgba(161, 161, 170, 0.3); /* zinc-400 */ border-radius: 6px; font-size: 0.875rem; color: rgb(161 161 170); /* zinc-400 */ }
|
||||
.publish-date-topright { position: absolute; top: 1rem; right: 1rem; font-size: 0.813rem; color: var(--color-text); padding: 0.4rem 0.75rem; text-shadow: 0 2px 4px rgba(0, 0, 0, 0.5); z-index: 10; }
|
||||
.reading-time { display: flex; align-items: center; gap: 0.5rem; padding: 0.375rem 0.75rem; background: var(--color-bg-elevated); border: 1px solid var(--color-border); border-radius: 6px; font-size: 0.875rem; color: var(--color-text-secondary); }
|
||||
.reading-time svg { font-size: 0.875rem; }
|
||||
.highlight-indicator { display: flex; align-items: center; gap: 0.5rem; padding: 0.375rem 0.75rem; background: rgba(99, 102, 241, 0.1); /* indigo-500 */ border: 1px solid rgba(99, 102, 241, 0.3); /* indigo-500 */ border-radius: 6px; font-size: 0.875rem; color: rgb(99 102 241); /* indigo-500 */ }
|
||||
.highlight-indicator { display: flex; align-items: center; gap: 0.5rem; padding: 0.375rem 0.75rem; background: rgba(99, 102, 241, 0.1); border: 1px solid rgba(99, 102, 241, 0.3); border-radius: 6px; font-size: 0.875rem; color: var(--color-primary); }
|
||||
.highlight-indicator svg { font-size: 0.875rem; }
|
||||
.reader-html { color: rgb(228 228 231); /* zinc-200 */ line-height: 1.6; word-wrap: break-word; overflow-wrap: break-word; word-break: break-word; font-family: var(--reading-font); font-size: var(--reading-font-size); }
|
||||
.reader-markdown { color: rgb(228 228 231); /* zinc-200 */ line-height: 1.7; font-family: var(--reading-font); font-size: var(--reading-font-size); }
|
||||
.reader-html { color: var(--color-text); line-height: 1.6; word-wrap: break-word; overflow-wrap: break-word; word-break: break-word; font-family: var(--reading-font); font-size: var(--reading-font-size); }
|
||||
.reader-markdown { color: var(--color-text); line-height: 1.7; font-family: var(--reading-font); font-size: var(--reading-font-size); }
|
||||
/* Ensure content is left-aligned even if source markup uses center */
|
||||
.reader .reader-html *, .reader .reader-markdown * { text-align: left !important; font-family: inherit !important; }
|
||||
.reader center, .reader [align="center"] { text-align: left !important; }
|
||||
@@ -49,7 +49,7 @@
|
||||
line-height: 1.2;
|
||||
margin-top: 2rem;
|
||||
margin-bottom: 1rem;
|
||||
color: rgb(244 244 245); /* zinc-100 */
|
||||
color: var(--color-text);
|
||||
}
|
||||
.reader-markdown h2, .reader-html h2 {
|
||||
font-size: 1.875rem; /* text-3xl */
|
||||
@@ -57,7 +57,7 @@
|
||||
line-height: 1.3;
|
||||
margin-top: 1.75rem;
|
||||
margin-bottom: 0.875rem;
|
||||
color: rgb(244 244 245); /* zinc-100 */
|
||||
color: var(--color-text);
|
||||
}
|
||||
.reader-markdown h3, .reader-html h3 {
|
||||
font-size: 1.5rem; /* text-2xl */
|
||||
@@ -65,7 +65,7 @@
|
||||
line-height: 1.4;
|
||||
margin-top: 1.5rem;
|
||||
margin-bottom: 0.75rem;
|
||||
color: rgb(244 244 245); /* zinc-100 */
|
||||
color: var(--color-text);
|
||||
}
|
||||
.reader-markdown h4, .reader-html h4 {
|
||||
font-size: 1.25rem; /* text-xl */
|
||||
@@ -73,7 +73,7 @@
|
||||
line-height: 1.4;
|
||||
margin-top: 1.25rem;
|
||||
margin-bottom: 0.625rem;
|
||||
color: rgb(228 228 231); /* zinc-200 */
|
||||
color: var(--color-text);
|
||||
}
|
||||
.reader-markdown h5, .reader-html h5 {
|
||||
font-size: 1.125rem; /* text-lg */
|
||||
@@ -81,7 +81,7 @@
|
||||
line-height: 1.4;
|
||||
margin-top: 1rem;
|
||||
margin-bottom: 0.5rem;
|
||||
color: rgb(228 228 231); /* zinc-200 */
|
||||
color: var(--color-text);
|
||||
}
|
||||
.reader-markdown h6, .reader-html h6 {
|
||||
font-size: 1rem; /* text-base */
|
||||
@@ -89,7 +89,7 @@
|
||||
line-height: 1.4;
|
||||
margin-top: 1rem;
|
||||
margin-bottom: 0.5rem;
|
||||
color: rgb(228 228 231); /* zinc-200 */
|
||||
color: var(--color-text);
|
||||
}
|
||||
.reader-markdown p { margin: 0.5rem 0; }
|
||||
.reader-html p, .reader-html div, .reader-html span, .reader-html li, .reader-html td, .reader-html th { font-size: 1em !important; }
|
||||
@@ -107,7 +107,7 @@
|
||||
.reader-markdown li, .reader-html li {
|
||||
margin: 0.375rem 0;
|
||||
line-height: 1.6;
|
||||
color: rgb(228 228 231); /* zinc-200 */
|
||||
color: var(--color-text);
|
||||
}
|
||||
.reader-markdown ul ul, .reader-markdown ol ul, .reader-html ul ul, .reader-html ol ul {
|
||||
list-style-type: circle;
|
||||
@@ -128,16 +128,16 @@
|
||||
.reader-markdown blockquote p, .reader-html blockquote p { margin: 0.5rem 0; }
|
||||
.reader-markdown blockquote p:first-child, .reader-html blockquote p:first-child { margin-top: 0; }
|
||||
.reader-markdown blockquote p:last-child, .reader-html blockquote p:last-child { margin-bottom: 0; }
|
||||
.reader-markdown a { color: rgb(96 165 250); /* blue-400 */ text-decoration: none; }
|
||||
.reader-markdown a { color: var(--color-primary); text-decoration: none; }
|
||||
.reader-markdown a:hover { text-decoration: underline; }
|
||||
.reader-markdown code { background: rgb(30 30 30); /* ~zinc-850 */ border: 1px solid rgb(63 63 70); /* zinc-700 */ border-radius: 4px; padding: 0.15rem 0.4rem; font-size: 0.9em; font-family: 'Monaco', 'Menlo', 'Consolas', 'Courier New', monospace; }
|
||||
.reader-markdown pre { background: rgb(30 30 30); /* ~zinc-850 */ border: 1px solid rgb(63 63 70); /* zinc-700 */ border-radius: 8px; padding: 1rem; overflow-x: auto; margin: 1rem 0; line-height: 1.5; font-family: 'Monaco', 'Menlo', 'Consolas', 'Courier New', monospace; }
|
||||
.reader-markdown code { background: var(--color-bg-subtle); border: 1px solid var(--color-border); border-radius: 4px; padding: 0.15rem 0.4rem; font-size: 0.9em; font-family: 'Monaco', 'Menlo', 'Consolas', 'Courier New', monospace; }
|
||||
.reader-markdown pre { background: var(--color-bg-subtle); border: 1px solid var(--color-border); border-radius: 8px; padding: 1rem; overflow-x: auto; margin: 1rem 0; line-height: 1.5; font-family: 'Monaco', 'Menlo', 'Consolas', 'Courier New', monospace; }
|
||||
.reader-markdown pre code { background: transparent; border: none; padding: 0; font-size: 0.9em; display: block; }
|
||||
/* Prism.js enhancements */
|
||||
.reader-markdown pre[class*="language-"] { background: rgb(30 30 30); /* ~zinc-850 */ border: 1px solid rgb(63 63 70); /* zinc-700 */ }
|
||||
.reader-markdown pre[class*="language-"] { background: var(--color-bg-subtle); border: 1px solid var(--color-border); }
|
||||
.reader-markdown code[class*="language-"] { background: transparent; text-shadow: none; }
|
||||
.reader-html pre { background: rgb(30 30 30); /* ~zinc-850 */ border: 1px solid rgb(63 63 70); /* zinc-700 */ border-radius: 8px; padding: 1rem; overflow-x: auto; margin: 1rem 0; font-family: 'Monaco', 'Menlo', 'Consolas', 'Courier New', monospace; }
|
||||
.reader-html code { background: rgb(30 30 30); /* ~zinc-850 */ border: 1px solid rgb(63 63 70); /* zinc-700 */ border-radius: 4px; padding: 0.15rem 0.4rem; font-size: 0.9em; font-family: 'Monaco', 'Menlo', 'Consolas', 'Courier New', monospace; }
|
||||
.reader-html pre { background: var(--color-bg-subtle); border: 1px solid var(--color-border); border-radius: 8px; padding: 1rem; overflow-x: auto; margin: 1rem 0; font-family: 'Monaco', 'Menlo', 'Consolas', 'Courier New', monospace; }
|
||||
.reader-html code { background: var(--color-bg-subtle); border: 1px solid var(--color-border); border-radius: 4px; padding: 0.15rem 0.4rem; font-size: 0.9em; font-family: 'Monaco', 'Menlo', 'Consolas', 'Courier New', monospace; }
|
||||
.reader-html pre code { background: transparent; border: none; padding: 0; display: block; }
|
||||
/* Mobile: prevent code blocks from causing horizontal overflow */
|
||||
@media (max-width: 768px) {
|
||||
@@ -182,17 +182,17 @@
|
||||
/* Article menu */
|
||||
.article-menu-container { display: flex; justify-content: flex-end; padding: 1.5rem 0 0.5rem; margin-top: 2rem; }
|
||||
.article-menu-wrapper { position: relative; }
|
||||
.article-menu-btn { background: none; border: none; color: rgb(161 161 170); /* zinc-400 */ cursor: pointer; padding: 0.5rem 0.75rem; font-size: 0.875rem; display: flex; align-items: center; gap: 0.5rem; transition: all 0.2s ease; border-radius: 6px; }
|
||||
.article-menu-btn:hover { color: rgb(99 102 241); /* indigo-500 */ background: rgba(99, 102, 241, 0.1); }
|
||||
.article-menu { position: absolute; right: 0; top: calc(100% + 4px); background: rgb(39 39 42); /* zinc-800 */ border: 1px solid rgb(82 82 91); /* zinc-600 */ border-radius: 6px; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); z-index: 1000; min-width: 180px; overflow: hidden; }
|
||||
.article-menu-item { width: 100%; background: none; border: none; color: rgb(228 228 231); /* zinc-200 */ padding: 0.75rem 1rem; font-size: 0.875rem; display: flex; align-items: center; gap: 0.75rem; cursor: pointer; transition: all 0.15s ease; text-align: left; white-space: nowrap; }
|
||||
.article-menu-item:hover { background: rgba(99, 102, 241, 0.15); color: rgb(255 255 255); /* white */ }
|
||||
.article-menu-btn { background: none; border: none; color: var(--color-text-secondary); cursor: pointer; padding: 0.5rem 0.75rem; font-size: 0.875rem; display: flex; align-items: center; gap: 0.5rem; transition: all 0.2s ease; border-radius: 6px; }
|
||||
.article-menu-btn:hover { color: var(--color-primary); background: rgba(99, 102, 241, 0.1); }
|
||||
.article-menu { position: absolute; right: 0; top: calc(100% + 4px); background: var(--color-bg-elevated); border: 1px solid var(--color-border-subtle); border-radius: 6px; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); z-index: 1000; min-width: 180px; overflow: hidden; }
|
||||
.article-menu-item { width: 100%; background: none; border: none; color: var(--color-text); padding: 0.75rem 1rem; font-size: 0.875rem; display: flex; align-items: center; gap: 0.75rem; cursor: pointer; transition: all 0.15s ease; text-align: left; white-space: nowrap; }
|
||||
.article-menu-item:hover { background: rgba(99, 102, 241, 0.15); color: var(--color-text); }
|
||||
.article-menu-item svg { font-size: 0.875rem; flex-shrink: 0; }
|
||||
|
||||
/* Mark as Read button */
|
||||
.mark-as-read-container { display: flex; justify-content: center; align-items: center; padding: 2rem 1rem; margin-top: 1rem; }
|
||||
.mark-as-read-btn { display: flex; align-items: center; gap: 0.5rem; padding: 0.75rem 1.5rem; background: rgb(39 39 42); /* zinc-800 */ color: rgb(228 228 231); /* zinc-200 */ border: 1px solid rgb(82 82 91); /* zinc-600 */ border-radius: 8px; font-size: 1rem; font-weight: 500; cursor: pointer; transition: all 0.2s ease; min-width: 160px; justify-content: center; }
|
||||
.mark-as-read-btn:hover:not(:disabled) { background: rgb(63 63 70); /* zinc-700 */ border-color: rgb(113 113 122); /* zinc-500 */ transform: translateY(-1px); }
|
||||
.mark-as-read-btn { display: flex; align-items: center; gap: 0.5rem; padding: 0.75rem 1.5rem; background: var(--color-bg-elevated); color: var(--color-text); border: 1px solid var(--color-border-subtle); border-radius: 8px; font-size: 1rem; font-weight: 500; cursor: pointer; transition: all 0.2s ease; min-width: 160px; justify-content: center; }
|
||||
.mark-as-read-btn:hover:not(:disabled) { background: var(--color-border); border-color: var(--color-text-muted); transform: translateY(-1px); }
|
||||
.mark-as-read-btn:active:not(:disabled) { transform: translateY(0); }
|
||||
.mark-as-read-btn:disabled { opacity: 0.6; cursor: not-allowed; }
|
||||
.mark-as-read-btn svg { font-size: 1.1rem; }
|
||||
@@ -228,7 +228,7 @@
|
||||
@media (max-width: 768px) {
|
||||
.reader-header-overlay .reader-summary.hide-on-mobile { display: none; }
|
||||
.reader-summary-below-image { display: block; padding: 0 0 1.5rem 0; margin-top: -1rem; }
|
||||
.reader-summary-below-image .reader-summary { color: #aaa; font-size: 1rem; line-height: 1.6; margin: 0; }
|
||||
.reader-summary-below-image .reader-summary { color: var(--color-text-secondary); font-size: 1rem; line-height: 1.6; margin: 0; }
|
||||
.reader-hero-image { min-height: 280px; max-height: 400px; height: 50vh; }
|
||||
.reader-hero-image img { height: 100%; width: 100%; object-fit: cover; object-position: center; }
|
||||
.reader-header-overlay { padding: 1.5rem 1rem 1rem; }
|
||||
@@ -237,4 +237,3 @@
|
||||
|
||||
/* Reading Progress Indicator - now using Tailwind utilities in component */
|
||||
|
||||
|
||||
|
||||
@@ -6,37 +6,37 @@
|
||||
.settings-content { overflow-y: auto; flex: 1; margin-bottom: 1rem; text-align: left; padding: 0 0.25rem 2rem 0.25rem; }
|
||||
.settings-section { margin-bottom: 2.5rem; }
|
||||
.settings-section:last-child { margin-bottom: 0; }
|
||||
.section-title { font-size: 1rem; font-weight: 600; color: rgb(255 255 255); /* white */ margin: 0 0 1rem 0; padding-bottom: 0.5rem; border-bottom: 1px solid rgb(63 63 70); /* zinc-700 */ text-transform: uppercase; letter-spacing: 0.05em; }
|
||||
.section-title { font-size: 1rem; font-weight: 600; color: var(--color-text); margin: 0 0 1rem 0; padding-bottom: 0.5rem; border-bottom: 1px solid var(--color-border); text-transform: uppercase; letter-spacing: 0.05em; }
|
||||
.settings-footer { display: flex; justify-content: flex-start; padding: 1rem 0 0.5rem 0; flex-shrink: 0; }
|
||||
.settings-footer .btn-primary { background: rgb(99 102 241); /* indigo-500 */ color: white; border: none; padding: 0.75rem 1.5rem; border-radius: 4px; font-size: 1rem; cursor: pointer; transition: background-color 0.2s; display: flex; align-items: center; gap: 0.5rem; }
|
||||
.settings-footer .btn-primary:hover:not(:disabled) { background: rgb(79 70 229); /* indigo-600 */ }
|
||||
.settings-footer .btn-primary { background: var(--color-primary); color: white; border: none; padding: 0.75rem 1.5rem; border-radius: 4px; font-size: 1rem; cursor: pointer; transition: background-color 0.2s; display: flex; align-items: center; gap: 0.5rem; }
|
||||
.settings-footer .btn-primary:hover:not(:disabled) { background: var(--color-primary-hover); }
|
||||
.settings-footer .btn-primary:disabled { opacity: 0.6; cursor: not-allowed; }
|
||||
|
||||
/* Setting groups */
|
||||
.setting-group { margin-bottom: 1.5rem; }
|
||||
.setting-label { display: block; margin-bottom: 0.75rem; font-size: 0.9rem; font-weight: 500; color: rgb(212 212 216); /* zinc-300 */ }
|
||||
.setting-label { display: block; margin-bottom: 0.75rem; font-size: 0.9rem; font-weight: 500; color: var(--color-text); }
|
||||
|
||||
/* Zap splits preset buttons */
|
||||
.zap-preset-buttons { display: flex; gap: 0.5rem; flex-wrap: wrap; }
|
||||
.zap-preset-btn {
|
||||
padding: 0.625rem 1.25rem;
|
||||
background: rgb(39 39 42); /* zinc-800 */
|
||||
border: 1px solid rgb(82 82 91); /* zinc-600 */
|
||||
background: var(--color-bg-elevated);
|
||||
border: 1px solid var(--color-border-subtle);
|
||||
border-radius: 6px;
|
||||
color: rgb(212 212 216); /* zinc-300 */
|
||||
color: var(--color-text);
|
||||
font-size: 0.9rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
font-weight: 500;
|
||||
}
|
||||
.zap-preset-btn:hover {
|
||||
background: rgb(63 63 70); /* zinc-700 */
|
||||
border-color: rgb(99 102 241); /* indigo-500 */
|
||||
color: rgb(255 255 255); /* white */
|
||||
background: var(--color-border);
|
||||
border-color: var(--color-primary);
|
||||
color: var(--color-text);
|
||||
}
|
||||
.zap-preset-btn.active {
|
||||
background: rgb(99 102 241); /* indigo-500 */
|
||||
border-color: rgb(99 102 241); /* indigo-500 */
|
||||
background: var(--color-primary);
|
||||
border-color: var(--color-primary);
|
||||
color: rgb(255 255 255); /* white */
|
||||
}
|
||||
|
||||
@@ -47,14 +47,14 @@
|
||||
justify-content: space-between;
|
||||
margin-bottom: 0.5rem;
|
||||
font-size: 0.85rem;
|
||||
color: rgb(161 161 170); /* zinc-400 */
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
.zap-split-label { font-weight: 500; }
|
||||
.zap-split-slider {
|
||||
width: 100%;
|
||||
height: 8px;
|
||||
border-radius: 4px;
|
||||
background: rgb(39 39 42); /* zinc-800 */
|
||||
background: var(--color-bg-elevated);
|
||||
outline: none;
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
@@ -64,36 +64,36 @@
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
background: rgb(99 102 241); /* indigo-500 */
|
||||
background: var(--color-primary);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
.zap-split-slider::-webkit-slider-thumb:hover {
|
||||
background: rgb(79 70 229); /* indigo-600 */
|
||||
background: var(--color-primary-hover);
|
||||
transform: scale(1.1);
|
||||
}
|
||||
.zap-split-slider::-moz-range-thumb {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
background: rgb(99 102 241); /* indigo-500 */
|
||||
background: var(--color-primary);
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
.zap-split-slider::-moz-range-thumb:hover {
|
||||
background: rgb(79 70 229); /* indigo-600 */
|
||||
background: var(--color-primary-hover);
|
||||
transform: scale(1.1);
|
||||
}
|
||||
.zap-split-description {
|
||||
margin-top: 1.5rem;
|
||||
padding: 1rem;
|
||||
background: rgb(39 39 42); /* zinc-800 */
|
||||
border: 1px solid rgb(63 63 70); /* zinc-700 */
|
||||
background: var(--color-bg-elevated);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 6px;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.5;
|
||||
color: rgb(161 161 170); /* zinc-400 */
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
/* Relay items */
|
||||
@@ -146,4 +146,3 @@
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/* Toast Notification */
|
||||
.toast { position: fixed; top: 2rem; right: 2rem; background: rgb(24 24 27); /* zinc-900 */ color: rgb(255 255 255); /* white */ padding: 1rem 1.5rem; border-radius: 8px; border: 1px solid rgb(63 63 70); /* zinc-700 */ display: flex; align-items: center; gap: 0.75rem; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); animation: toast-slide-in 0.3s ease-out; z-index: 9999; font-size: 0.95rem; }
|
||||
.toast { position: fixed; top: 2rem; right: 2rem; background: var(--color-bg); color: var(--color-text); padding: 1rem 1.5rem; border-radius: 8px; border: 1px solid var(--color-border); display: flex; align-items: center; gap: 0.75rem; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); animation: toast-slide-in 0.3s ease-out; z-index: 9999; font-size: 0.95rem; }
|
||||
@media (max-width: 768px) {
|
||||
.toast { top: auto; bottom: calc(1rem + var(--safe-area-bottom)); right: 1rem; left: 1rem; max-width: calc(100% - 2rem); }
|
||||
@keyframes toast-slide-in { from { transform: translateY(100px); opacity: 0; } to { transform: translateY(0); opacity: 1; } }
|
||||
@@ -10,4 +10,3 @@
|
||||
.toast-error svg { color: rgb(220 38 38); /* red-600 */ }
|
||||
@keyframes toast-slide-in { from { transform: translateX(400px); opacity: 0; } to { transform: translateX(0); opacity: 1; } }
|
||||
|
||||
|
||||
|
||||
@@ -111,7 +111,7 @@
|
||||
max-width: 320px;
|
||||
height: 100vh;
|
||||
height: 100dvh;
|
||||
background: rgb(24 24 27); /* zinc-900 */
|
||||
background: var(--color-bg);
|
||||
z-index: 1001; /* Above backdrop */
|
||||
transition: transform 0.3s ease;
|
||||
box-shadow: none;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* Highlights panel layout and interactions */
|
||||
.highlights-container {
|
||||
background: rgb(24 24 27); /* zinc-900 */
|
||||
background: var(--color-bg);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
@@ -25,8 +25,8 @@
|
||||
}
|
||||
|
||||
.highlights-container.collapsed .toggle-highlights-btn {
|
||||
background: rgb(39 39 42); /* zinc-800 */
|
||||
color: rgb(228 228 231); /* zinc-200 */
|
||||
background: var(--color-bg-elevated);
|
||||
color: var(--color-text);
|
||||
border: none;
|
||||
padding: 0;
|
||||
border-radius: 0;
|
||||
@@ -39,7 +39,7 @@
|
||||
height: 36px;
|
||||
}
|
||||
|
||||
.highlights-container.collapsed .toggle-highlights-btn:hover { background: rgb(63 63 70); /* zinc-700 */ color: rgb(255 255 255); /* white */ }
|
||||
.highlights-container.collapsed .toggle-highlights-btn:hover { background: var(--color-border); color: var(--color-text); }
|
||||
.highlights-container.collapsed .toggle-highlights-btn:active { transform: translateY(1px); }
|
||||
.highlights-container.collapsed .toggle-highlights-btn.with-icon { width: auto; padding: 0 0.5rem; gap: 0.5rem; }
|
||||
|
||||
@@ -48,8 +48,8 @@
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0.75rem 1rem;
|
||||
border-bottom: 1px solid rgb(63 63 70); /* zinc-700 */
|
||||
background: rgb(24 24 27); /* zinc-900 */
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
background: var(--color-bg);
|
||||
border-radius: 12px 12px 0 0;
|
||||
}
|
||||
|
||||
@@ -65,32 +65,32 @@
|
||||
|
||||
.highlights-title { display: flex; align-items: center; gap: 0.5rem; }
|
||||
.highlights-title h3 { margin: 0; font-size: 1rem; font-weight: 600; }
|
||||
.highlights-title .count { color: rgb(161 161 170); /* zinc-400 */ font-size: 0.875rem; }
|
||||
.highlights-title .count { color: var(--color-text-secondary); font-size: 0.875rem; }
|
||||
|
||||
.highlight-mode-toggle { display: flex; gap: 0.25rem; padding: 0.25rem; background: rgba(255, 255, 255, 0.05); border-radius: 4px; }
|
||||
.highlight-mode-toggle .mode-btn { background: none; border: none; color: rgb(161 161 170); /* zinc-400 */ cursor: pointer; padding: 0.375rem 0.5rem; border-radius: 3px; transition: all 0.2s; font-size: 0.9rem; }
|
||||
.highlight-mode-toggle .mode-btn:hover { background: rgba(255, 255, 255, 0.1); color: rgb(255 255 255); /* white */ }
|
||||
.highlight-mode-toggle .mode-btn.active { background: rgb(99 102 241); /* indigo-500 */ color: rgb(255 255 255); /* white */ }
|
||||
.highlight-mode-toggle .mode-btn { background: none; border: none; color: var(--color-text-secondary); cursor: pointer; padding: 0.375rem 0.5rem; border-radius: 3px; transition: all 0.2s; font-size: 0.9rem; }
|
||||
.highlight-mode-toggle .mode-btn:hover { background: rgba(255, 255, 255, 0.1); color: var(--color-text); }
|
||||
.highlight-mode-toggle .mode-btn.active { background: var(--color-primary); color: rgb(255 255 255); /* white */ }
|
||||
|
||||
/* Three-level highlight toggles */
|
||||
.highlight-level-toggles { display: flex; gap: 0.25rem; padding: 0.25rem; background: rgba(255, 255, 255, 0.05); border-radius: 4px; }
|
||||
|
||||
.highlights-loading,
|
||||
.highlights-empty { display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 2rem 1rem; color: rgb(161 161 170); /* zinc-400 */ text-align: center; gap: 0.5rem; }
|
||||
.highlights-empty svg { color: rgb(113 113 122); /* zinc-500 */ margin-bottom: 0.5rem; }
|
||||
.empty-hint { font-size: 0.875rem; color: rgb(113 113 122); /* zinc-500 */ margin-top: 0.5rem; }
|
||||
.highlights-empty { display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 2rem 1rem; color: var(--color-text-secondary); text-align: center; gap: 0.5rem; }
|
||||
.highlights-empty svg { color: var(--color-text-muted); margin-bottom: 0.5rem; }
|
||||
.empty-hint { font-size: 0.875rem; color: var(--color-text-muted); margin-top: 0.5rem; }
|
||||
|
||||
.highlights-list { overflow-y: auto; padding: 1rem; display: flex; flex-direction: column; gap: 0.75rem; }
|
||||
.highlight-item { background: rgb(30 30 30); /* ~zinc-850 */ border: 1px solid rgb(63 63 70); /* zinc-700 */ border-radius: 8px; padding: 0; display: flex; transition: border-color 0.2s ease; position: relative; }
|
||||
.highlight-item:hover { border-color: rgb(99 102 241); /* indigo-500 */ }
|
||||
.highlight-item.selected { border-color: rgb(99 102 241); /* indigo-500 */ background: rgb(39 39 42); /* zinc-800 */ box-shadow: 0 0 0 2px rgba(99, 102, 241, 0.3); }
|
||||
.highlight-item { background: var(--color-bg-subtle); border: 1px solid var(--color-border); border-radius: 8px; padding: 0; display: flex; transition: border-color 0.2s ease; position: relative; }
|
||||
.highlight-item:hover { border-color: var(--color-primary); }
|
||||
.highlight-item.selected { border-color: var(--color-primary); background: var(--color-bg-elevated); box-shadow: 0 0 0 2px rgba(99, 102, 241, 0.3); }
|
||||
|
||||
/* Compact button for highlight cards */
|
||||
.compact-button { background: none; border: none; color: rgb(161 161 170); /* zinc-400 */ cursor: pointer; padding: 0.25rem; font-size: 0.75rem; display: flex; align-items: center; justify-content: center; gap: 0.25rem; transition: all 0.2s ease; border-radius: 4px; min-width: 20px; min-height: 20px; }
|
||||
.compact-button:hover { color: rgb(212 212 216); /* zinc-300 */ background: rgba(255, 255, 255, 0.05); }
|
||||
.compact-button { background: none; border: none; color: var(--color-text-secondary); cursor: pointer; padding: 0.25rem; font-size: 0.75rem; display: flex; align-items: center; justify-content: center; gap: 0.25rem; transition: all 0.2s ease; border-radius: 4px; min-width: 20px; min-height: 20px; }
|
||||
.compact-button:hover { color: var(--color-text); background: rgba(255, 255, 255, 0.05); }
|
||||
.compact-button:active { transform: scale(0.95); }
|
||||
.compact-button:disabled { opacity: 0.5; cursor: not-allowed; }
|
||||
.compact-button:disabled:hover { background: none; color: rgb(161 161 170); /* zinc-400 */ transform: none; }
|
||||
.compact-button:disabled:hover { background: none; color: var(--color-text-secondary); transform: none; }
|
||||
|
||||
.highlight-header { position: absolute; top: 0; left: 0; right: 0; padding: 0.25rem 0.5rem; display: flex; align-items: center; justify-content: flex-end; pointer-events: none; border-top-left-radius: 8px; border-top-right-radius: 8px; transition: border-color 0.2s ease; }
|
||||
.highlight-header .compact-button { pointer-events: auto; }
|
||||
@@ -126,13 +126,18 @@
|
||||
.highlight-item.level-friends .highlight-quote-icon { color: var(--highlight-color-friends, #f97316); }
|
||||
.highlight-item.level-nostrverse .highlight-quote-icon { color: var(--highlight-color-nostrverse, #9333ea); }
|
||||
|
||||
.highlight-content { flex: 1; display: flex; flex-direction: column; gap: 0.5rem; padding: 2.25rem 0.75rem; }
|
||||
.highlight-text { margin: 0; padding: 0; font-style: italic; color: rgb(228 228 231); /* zinc-200 */ line-height: 1.6; border-left: none; font-size: 0.95rem; }
|
||||
.highlight-comment { margin-top: 0.5rem; padding: 0.75rem; background: rgba(99, 102, 241, 0.1); border-left: 3px solid rgb(99 102 241); /* indigo-500 */ border-radius: 4px; font-size: 0.875rem; color: rgb(228 228 231); /* zinc-200 */ line-height: 1.5; }
|
||||
.highlight-content { flex: 1; display: flex; flex-direction: column; gap: 0.5rem; padding: 2.25rem 0.75rem 2.5rem; }
|
||||
.highlight-text { margin: 0; padding: 0 0 0 1.25rem; font-style: italic; color: var(--color-text); line-height: 1.6; border-left: none; font-size: 0.95rem; }
|
||||
.highlight-comment { margin-top: 0.5rem; margin-left: 1.25rem; padding: 0.75rem; border-left: 3px solid; border-radius: 4px; font-size: 0.875rem; color: var(--color-text); line-height: 1.5; }
|
||||
|
||||
.highlight-footer { position: absolute; bottom: 0; left: 0; right: 0; display: flex; align-items: center; justify-content: space-between; padding: 0.25rem 0.5rem; font-size: 0.8rem; color: rgb(161 161 170); /* zinc-400 */ border-bottom-left-radius: 8px; border-bottom-right-radius: 8px; transition: border-color 0.2s ease; }
|
||||
/* Level-colored comments */
|
||||
.highlight-item.level-mine .highlight-comment { background: color-mix(in srgb, var(--highlight-color-mine, #ffff00) 10%, transparent); border-left-color: var(--highlight-color-mine, #ffff00); }
|
||||
.highlight-item.level-friends .highlight-comment { background: color-mix(in srgb, var(--highlight-color-friends, #f97316) 10%, transparent); border-left-color: var(--highlight-color-friends, #f97316); }
|
||||
.highlight-item.level-nostrverse .highlight-comment { background: color-mix(in srgb, var(--highlight-color-nostrverse, #9333ea) 10%, transparent); border-left-color: var(--highlight-color-nostrverse, #9333ea); }
|
||||
|
||||
.highlight-footer { position: absolute; bottom: 0; left: 0; right: 0; display: flex; align-items: center; justify-content: space-between; padding: 0.25rem 0.5rem; font-size: 0.8rem; color: var(--color-text-secondary); border-bottom-left-radius: 8px; border-bottom-right-radius: 8px; transition: border-color 0.2s ease; }
|
||||
.highlight-footer-left { display: flex; align-items: center; gap: 0.4rem; min-width: 0; }
|
||||
.highlight-author { color: rgb(212 212 216); /* zinc-300 */ font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 100%; display: inline-flex; align-items: center; min-height: 28px; }
|
||||
.highlight-author { color: var(--color-text); font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 100%; display: inline-flex; align-items: center; min-height: 28px; }
|
||||
|
||||
/* Ensure relay indicator in footer uses normal flow and matches CompactButton spacing */
|
||||
.highlight-item .highlight-footer .highlight-relay-indicator {
|
||||
@@ -143,11 +148,10 @@
|
||||
padding: 0.25rem; /* CompactButton base */
|
||||
}
|
||||
.highlight-menu-wrapper { position: relative; flex-shrink: 0; display: flex; align-items: center; }
|
||||
.highlight-menu { position: absolute; right: 0; top: calc(100% + 4px); background: rgb(39 39 42); /* zinc-800 */ border: 1px solid rgb(82 82 91); /* zinc-600 */ border-radius: 6px; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); z-index: 1000; min-width: 160px; overflow: hidden; }
|
||||
.highlight-menu-item { width: 100%; background: none; border: none; color: rgb(228 228 231); /* zinc-200 */ padding: 0.625rem 0.875rem; font-size: 0.875rem; display: flex; align-items: center; gap: 0.625rem; cursor: pointer; transition: all 0.15s ease; text-align: left; white-space: nowrap; }
|
||||
.highlight-menu-item:hover { background: rgba(99, 102, 241, 0.15); color: rgb(255 255 255); /* white */ }
|
||||
.highlight-menu { position: absolute; right: 0; top: calc(100% + 4px); background: var(--color-bg-elevated); border: 1px solid var(--color-border-subtle); border-radius: 6px; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); z-index: 1000; min-width: 160px; overflow: hidden; }
|
||||
.highlight-menu-item { width: 100%; background: none; border: none; color: var(--color-text); padding: 0.625rem 0.875rem; font-size: 0.875rem; display: flex; align-items: center; gap: 0.625rem; cursor: pointer; transition: all 0.15s ease; text-align: left; white-space: nowrap; }
|
||||
.highlight-menu-item:hover { background: rgba(99, 102, 241, 0.15); color: var(--color-text); }
|
||||
.highlight-menu-item:disabled { opacity: 0.5; cursor: not-allowed; }
|
||||
.highlight-menu-item-danger:hover { background: rgba(255, 68, 68, 0.15); color: rgb(239 68 68); /* red-500 */ }
|
||||
.highlight-menu-item svg { font-size: 0.875rem; flex-shrink: 0; }
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* Bookmarks and sidebar layout */
|
||||
.bookmarks-container {
|
||||
background: rgb(24 24 27); /* zinc-900 */
|
||||
background: var(--color-bg);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
@@ -20,7 +20,7 @@
|
||||
.bookmarks-container .view-mode-controls {
|
||||
margin-top: auto;
|
||||
padding: 1rem;
|
||||
border-top: 1px solid rgb(63 63 70); /* zinc-700 */
|
||||
border-top: 1px solid var(--color-border);
|
||||
background: transparent;
|
||||
border-radius: 0;
|
||||
}
|
||||
@@ -41,15 +41,15 @@
|
||||
justify-content: space-between;
|
||||
gap: 0.75rem;
|
||||
padding: 0.75rem 1rem;
|
||||
background: rgb(24 24 27); /* zinc-900 */
|
||||
border-bottom: 1px solid rgb(63 63 70); /* zinc-700 */
|
||||
background: var(--color-bg);
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
/* Mobile: add borders and rounded corners */
|
||||
@media (max-width: 768px) {
|
||||
.sidebar-header-bar {
|
||||
border: 1px solid rgb(63 63 70); /* zinc-700 */
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 12px 12px 0 0;
|
||||
}
|
||||
}
|
||||
@@ -95,10 +95,10 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgb(39 39 42); /* zinc-800 */
|
||||
border: 1px solid rgb(82 82 91); /* zinc-600 */
|
||||
background: var(--color-bg-elevated);
|
||||
border: 1px solid var(--color-border-subtle);
|
||||
flex-shrink: 0;
|
||||
color: rgb(228 228 231); /* zinc-200 */
|
||||
color: var(--color-text);
|
||||
box-sizing: border-box;
|
||||
padding: 0;
|
||||
}
|
||||
@@ -108,8 +108,8 @@
|
||||
|
||||
.sidebar-header-bar .toggle-sidebar-btn {
|
||||
background: transparent;
|
||||
color: rgb(228 228 231); /* zinc-200 */
|
||||
border: 1px solid rgb(82 82 91); /* zinc-600 */
|
||||
color: var(--color-text);
|
||||
border: 1px solid var(--color-border-subtle);
|
||||
padding: 0;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
@@ -123,7 +123,7 @@
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.sidebar-header-bar .toggle-sidebar-btn:hover { background: rgb(39 39 42); /* zinc-800 */ color: rgb(255 255 255); /* white */ }
|
||||
.sidebar-header-bar .toggle-sidebar-btn:hover { background: var(--color-bg-elevated); color: var(--color-text); }
|
||||
.sidebar-header-bar .toggle-sidebar-btn:active { transform: translateY(1px); }
|
||||
|
||||
.bookmarks-container.collapsed {
|
||||
@@ -136,8 +136,8 @@
|
||||
}
|
||||
|
||||
.bookmarks-container.collapsed .toggle-sidebar-btn {
|
||||
background: rgb(39 39 42); /* zinc-800 */
|
||||
color: rgb(228 228 231); /* zinc-200 */
|
||||
background: var(--color-bg-elevated);
|
||||
color: var(--color-text);
|
||||
border: none;
|
||||
padding: 0;
|
||||
border-radius: 0;
|
||||
@@ -151,22 +151,21 @@
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.bookmarks-container.collapsed .toggle-sidebar-btn:hover { background: rgb(63 63 70); /* zinc-700 */ color: rgb(255 255 255); /* white */ }
|
||||
.bookmarks-container.collapsed .toggle-sidebar-btn:hover { background: var(--color-border); color: var(--color-text); }
|
||||
.bookmarks-container.collapsed .toggle-sidebar-btn:active { transform: translateY(1px); }
|
||||
.bookmarks-container.collapsed .toggle-sidebar-btn.with-icon { width: auto; padding: 0 0.5rem; gap: 0.5rem; }
|
||||
.bookmarks-container.collapsed .toggle-sidebar-btn .glow-blue { color: rgb(99 102 241); /* indigo-500 */ filter: drop-shadow(0 0 4px rgba(99, 102, 241, 0.6)); }
|
||||
.bookmarks-container.collapsed .toggle-sidebar-btn .glow-blue { color: var(--color-primary); filter: drop-shadow(0 0 4px rgba(99, 102, 241, 0.6)); }
|
||||
|
||||
.user-info { margin: 0.5rem 0 0 0; color: rgb(161 161 170); /* zinc-400 */ font-size: 0.9rem; font-family: monospace; }
|
||||
.bookmark-count { color: rgb(113 113 122); /* zinc-500 */ font-size: 0.9rem; margin: 0.5rem 0; }
|
||||
.event-link { color: rgb(96 165 250); /* blue-400 */ text-decoration: none; font-weight: 500; }
|
||||
.user-info { margin: 0.5rem 0 0 0; color: var(--color-text-secondary); font-size: 0.9rem; font-family: monospace; }
|
||||
.bookmark-count { color: var(--color-text-muted); font-size: 0.9rem; margin: 0.5rem 0; }
|
||||
.event-link { color: var(--color-primary); text-decoration: none; font-weight: 500; }
|
||||
.event-link:hover { text-decoration: underline; }
|
||||
|
||||
.bookmark-urls { margin: 0.75rem 0; }
|
||||
.bookmark-url { display: block; margin: 0.25rem 0; color: rgb(59 130 246); /* blue-500 */ text-decoration: none; word-break: break-all; background: none; border: none; padding: 0; font: inherit; cursor: pointer; text-align: left; width: 100%; }
|
||||
.bookmark-url { display: block; margin: 0.25rem 0; color: var(--color-primary); text-decoration: none; word-break: break-all; background: none; border: none; padding: 0; font: inherit; cursor: pointer; text-align: left; width: 100%; }
|
||||
.bookmark-url:hover { text-decoration: underline; }
|
||||
|
||||
.url-row { display: flex; align-items: center; gap: 0.5rem; }
|
||||
.read-inline-btn { background: rgb(34 197 94); /* green-500 */ color: white; border: none; padding: 0.25rem 0.5rem; border-radius: 4px; cursor: pointer; }
|
||||
.read-inline-btn:hover { background: rgb(22 163 74); /* green-600 */ }
|
||||
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ export function extractNaddrUris(text: string): string[] {
|
||||
|
||||
/**
|
||||
* Decode a NIP-19 identifier and return a human-readable link
|
||||
* For articles (naddr), returns an internal app link
|
||||
* For articles (naddr) and profiles (npub/nprofile), returns internal app links
|
||||
* For other types, returns an external gateway link
|
||||
*/
|
||||
export function createNostrLink(encoded: string): string {
|
||||
@@ -51,7 +51,13 @@ export function createNostrLink(encoded: string): string {
|
||||
// For articles, link to our internal app route
|
||||
return `/a/${encoded}`
|
||||
case 'npub':
|
||||
case 'nprofile':
|
||||
// For profiles, link to our internal app route
|
||||
return `/p/${encoded}`
|
||||
case 'nprofile': {
|
||||
// For nprofile, convert to npub and link to our internal app route
|
||||
const npub = npubEncode(decoded.data.pubkey)
|
||||
return `/p/${npub}`
|
||||
}
|
||||
case 'note':
|
||||
case 'nevent':
|
||||
return getNostrUrl(encoded)
|
||||
|
||||
63
src/utils/theme.ts
Normal file
63
src/utils/theme.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
export type Theme = 'dark' | 'light' | 'system'
|
||||
export type DarkColorTheme = 'black' | 'midnight' | 'charcoal'
|
||||
export type LightColorTheme = 'paper-white' | 'sepia' | 'ivory'
|
||||
|
||||
let mediaQueryListener: ((e: MediaQueryListEvent) => void) | null = null
|
||||
|
||||
/**
|
||||
* Get the system's current theme preference
|
||||
*/
|
||||
export function getSystemTheme(): 'dark' | 'light' {
|
||||
if (typeof window === 'undefined') return 'dark'
|
||||
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply theme and color variant to the document root element
|
||||
* Handles 'system' theme by listening to OS preference changes
|
||||
*/
|
||||
export function applyTheme(
|
||||
theme: Theme,
|
||||
darkColorTheme: DarkColorTheme = 'midnight',
|
||||
lightColorTheme: LightColorTheme = 'sepia'
|
||||
): void {
|
||||
const root = document.documentElement
|
||||
|
||||
// Remove existing theme classes
|
||||
root.classList.remove('theme-dark', 'theme-light', 'theme-system')
|
||||
// Remove existing color theme classes
|
||||
root.classList.remove('dark-black', 'dark-midnight', 'dark-charcoal')
|
||||
root.classList.remove('light-paper-white', 'light-sepia', 'light-ivory')
|
||||
|
||||
// Clean up previous media query listener if exists
|
||||
if (mediaQueryListener) {
|
||||
window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', mediaQueryListener)
|
||||
mediaQueryListener = null
|
||||
}
|
||||
|
||||
if (theme === 'system') {
|
||||
root.classList.add('theme-system')
|
||||
|
||||
// Apply color themes for system mode (CSS will handle media query)
|
||||
root.classList.add(`dark-${darkColorTheme}`)
|
||||
root.classList.add(`light-${lightColorTheme}`)
|
||||
|
||||
// Listen for system theme changes
|
||||
mediaQueryListener = (e: MediaQueryListEvent) => {
|
||||
console.log('🎨 System theme changed to:', e.matches ? 'dark' : 'light')
|
||||
// The CSS media query handles the color changes automatically
|
||||
}
|
||||
|
||||
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', mediaQueryListener)
|
||||
} else {
|
||||
root.classList.add(`theme-${theme}`)
|
||||
// Apply appropriate color theme based on light/dark
|
||||
if (theme === 'dark') {
|
||||
root.classList.add(`dark-${darkColorTheme}`)
|
||||
} else {
|
||||
root.classList.add(`light-${lightColorTheme}`)
|
||||
}
|
||||
}
|
||||
|
||||
console.log('🎨 Applied theme:', theme, 'with colors:', { dark: darkColorTheme, light: lightColorTheme })
|
||||
}
|
||||
Reference in New Issue
Block a user