Compare commits

...

61 Commits

Author SHA1 Message Date
Gigi
fcc329cc7c chore: bump version to 0.6.5 2025-10-14 11:48:25 +02:00
Gigi
c9544e0fd2 feat: open highlight in native app when clicking timestamp
- Click timestamp to open highlight event in user's native Nostr app
- Reuses existing native link logic (nostr:nevent)
- Simple and DRY implementation
2025-10-14 11:40:30 +02:00
Gigi
d7906cfb95 fix: use article text color for highlight counter
- Change highlight indicator to var(--color-text)
- Matches main article text color for better readability
- More prominent and consistent with content
2025-10-14 11:38:59 +02:00
Gigi
13cd6aeb11 fix: use consistent text color for highlight counter
- Change highlight indicator color to var(--color-text-secondary)
- Matches reading time color for visual consistency
- Better readability in both light and dark modes
2025-10-14 11:38:19 +02:00
Gigi
d4821d18fb fix: improve highlight counter readability in light mode
- Make highlight indicator color theme-aware
- Only force white text color in overlay context (with hero image)
- Let CSS handle text color in regular header for better light mode support
- Fixes hard-to-read white text on light backgrounds
2025-10-14 11:37:13 +02:00
Gigi
b86bf48382 deps: add @fortawesome/free-regular-svg-icons
- Install FontAwesome regular icons package
- Required for faComments icon in HighlightItem component
2025-10-14 11:34:52 +02:00
Gigi
c595f94567 style: switch to regular comments icon
- Use faComments from @fortawesome/free-regular-svg-icons
- Replace solid faComment with regular faComments
- Provides lighter, outlined icon style per FontAwesome regular variant
2025-10-14 11:33:02 +02:00
Gigi
82058c0ef4 style: remove extra indent from highlight comments
- Remove margin-left from comment container
- Icon alone provides sufficient visual indentation
- Cleaner alignment with highlight content
2025-10-14 11:32:16 +02:00
Gigi
a1f3424b38 style: remove background from highlight comments
- Remove background color from comment boxes
- Keep only colored icon for visual distinction
- Cleaner, simpler appearance
2025-10-14 11:31:32 +02:00
Gigi
14ab749ef1 style: color comment icon by highlight level and remove border
- Remove border-left from highlight comments
- Color comment icon based on highlight level (mine/friends/nostrverse)
- Remove opacity from icon for clearer color representation
- Yellow for mine, orange for friends, purple for nostrverse
2025-10-14 11:30:07 +02:00
Gigi
61dd4b2089 style: flip comment icon horizontally
- Add flip='horizontal' prop to comment icon
- Better visual alignment with comment text
2025-10-14 11:29:26 +02:00
Gigi
fb2fe1cc63 feat(highlights): add comment icon to highlight comments
- Import and use faComment icon
- Display comment icon next to comment text
- Style with flexbox layout and slight opacity
- Icon aligns to top with comment text
- Visual indicator that distinguishes comments from highlights
2025-10-14 11:28:51 +02:00
Gigi
720f12ce1c feat(explore): color highlights by author level (mine/friends/nostrverse)
- Import and use classifyHighlights utility
- Track followed pubkeys from contact fetching
- Classify highlights using same logic as highlights sidebar
- Pass classified highlights with level to HighlightItem
- Highlights now show colored borders based on author:
  - Yellow for own highlights (mine)
  - Orange for friends' highlights
  - Purple for nostrverse highlights
- Keep code DRY by reusing existing classification logic
2025-10-14 11:26:19 +02:00
Gigi
423ebb403f fix: add retry mechanism for scroll-to-highlight in article content
- Replace single 100ms delay with retry mechanism
- Try up to 20 times (2 seconds total) to find highlight mark element
- Fixes timing issue when content is still loading from explore page
- Mark elements need time to be rendered after article loads
- Retry every 100ms until element is found or max attempts reached
- Improves reliability of highlight scrolling from external navigation
2025-10-14 11:24:35 +02:00
Gigi
c90fb66bb8 feat(explore): scroll to highlight and open sidebar on click
- Pass highlight ID and openHighlights flag via navigation state
- Add useEffect in Bookmarks to handle navigation state
- Open highlights sidebar when clicking highlight from explore
- Auto-scroll to selected highlight (handled by useHighlightInteractions)
- Clear state after handling to prevent re-triggering
- Enhanced UX for discovering and reading highlighted content
2025-10-14 11:22:34 +02:00
Gigi
188de7ab1d feat(explore): clicking highlight opens source article in reader
- Add handleHighlightClick handler in explore page
- For nostr-native articles: convert eventReference to naddr and navigate to /a/{naddr}
- For web URLs: navigate to /r/{encoded-url}
- Pass onHighlightClick to HighlightItem component
- Users can now click highlights to read the full source content
2025-10-14 11:20:17 +02:00
Gigi
0b1cf267a7 refactor: reorder explore tabs - highlights first, writings second
- Change default tab to highlights on /explore
- Reorder tab buttons in UI (highlights, then writings)
- Update route: /explore shows highlights, /explore/writings shows writings
- Update route detection logic in Bookmarks component
- Highlights are now the primary content on explore page
2025-10-14 11:08:42 +02:00
Gigi
19f68612a5 style: use highlighter icon instead of server icon in highlight items
- Replace faServer with faHighlighter in bottom left indicator
- Update import statement
- Keep plane icon for offline/local-only highlights
- More semantically appropriate icon for highlight items
2025-10-14 11:08:03 +02:00
Gigi
1b1600d6f2 fix: make explore tabs actually span full width
- Set width: 100% and max-width: 100% on tabs
- Add justify-content: flex-start for left alignment
- Tabs now properly extend to match grid width
2025-10-14 11:07:00 +02:00
Gigi
ce67c19ece style: make explore tabs extend to grid width
- Add specific styling for tabs in explore-header
- Tabs now span full width to match the grid below
- Maintain left alignment for consistency with grid layout
2025-10-14 11:04:59 +02:00
Gigi
f754ce3cfe fix: extract author pubkey directly from p tag in highlights
- Pass full highlight object to HighlightCitation component
- Extract author pubkey from p tag as fallback if highlight.author not set
- Add debug logging to track author resolution
- Fix TypeScript type errors with proper guards
- Ensure author profile resolution works correctly
2025-10-14 11:04:24 +02:00
Gigi
19a86525cb debug: add logging to track author pubkey and profile resolution 2025-10-14 11:03:30 +02:00
Gigi
29213ceb1c feat(highlights): add citation attribution to highlight items
- Create HighlightCitation component to show source attribution
- For nostr-native content: display as '— Author, Article Title'
- For web URLs: display hostname as '— domain.com'
- Automatically resolves article titles from event references
- Resolves author names from profile data
- Add styling for citation line below highlight text
- Keep code DRY by reusing existing articleTitleResolver service
2025-10-14 11:01:02 +02:00
Gigi
d25a9b1735 refactor: use existing HighlightItem component for consistency
- Remove custom HighlightCard component
- Use the same HighlightItem component used throughout the app
- Remove custom highlight card styles
- Keep code DRY and UI consistent
2025-10-14 10:57:00 +02:00
Gigi
0f03706166 style: add proper styling for highlight cards in explore grid
- Add gradient header with quote icon for visual distinction
- Style highlight text and comments for card view
- Ensure cards work well in grid layout
- Add mobile responsive styling for highlight cards
2025-10-14 10:56:18 +02:00
Gigi
b1f79e3844 fix: resolve type errors and remove unused code
- Remove unused handleHighlightDelete function
- Fix all TypeScript type errors by using correct Highlight properties
- Use created_at instead of timestamp
- Use content instead of text
- Use urlReference instead of url
- All lint checks and type checks now pass
2025-10-14 10:54:51 +02:00
Gigi
243d9b17ef chore(explore): update subtitle text
- Change subtitle to mention both highlights and blog posts
- Include 'friends and others' to reflect broader content scope
2025-10-14 10:50:51 +02:00
Gigi
50a6cf6499 fix(explore): remove max-width constraint for grid layout
- Remove me-tab-content wrapper that was limiting width to 600px
- Allow explore-grid to use full width for proper multi-column layout
- Blog posts now display in proper grid format
2025-10-14 10:49:36 +02:00
Gigi
8f7991e971 refactor(explore): use grid layout for highlights tab
- Change highlights from list view to grid/card view
- Match the visual style of the writings tab
- Keep tab structure at the top
- Explore page now shows more content at once
2025-10-14 10:46:15 +02:00
Gigi
0aba54bd23 feat(explore): add highlights tab to explore page
- Create fetchHighlightsFromAuthors function for fetching highlights from multiple contacts
- Add tab structure to Explore page (Writings and Highlights tabs)
- Update explore cache to handle both blog posts and highlights
- Add /explore/highlights route
- Keep UI consistent with /me page tab structure
- Implement pull-to-refresh for both tabs
- Add proper caching and streaming for highlights
2025-10-14 10:45:23 +02:00
Gigi
23833b2cff docs: update CHANGELOG.md for v0.6.4 release 2025-10-14 10:40:40 +02:00
Gigi
d5076ff53e chore: bump version to 0.6.4 2025-10-14 10:39:44 +02:00
Gigi
41e452be1e Merge pull request #6 from dergigi/themes
Add comprehensive theme support with light/dark modes
2025-10-14 10:38:54 +02:00
Gigi
f267df8f60 feat(ui): increase bottom padding in highlight cards
- Increase bottom padding from 0.75rem to 2.5rem
- Reduces gap between cards from 1rem to 0.75rem (user edit)
- Provides more breathing room between text and footer
- Improves readability and visual balance
2025-10-14 10:35:34 +02:00
Gigi
7426c9d1fc feat(ui): increase spacing between highlight cards
- Increase gap from 0.75rem to 1rem in highlights list
- Provides better visual breathing room between cards
- Improves overall readability and card separation
2025-10-14 10:34:48 +02:00
Gigi
93d0c1052b feat(ui): align highlight text with footer icons
- Add 1.25rem left padding to highlight text content
- Add 1.25rem left margin to highlight comments
- Text now starts roughly where the fa-server icon ends
- Improves visual alignment and readability of highlight cards
2025-10-14 10:34:07 +02:00
Gigi
6537650757 feat(ui): apply highlight marker style to active Highlights tab
- Use actual highlight visual treatment (marker style) on tab
- Text remains in semantic color (--color-text) for readability
- Background uses 35% highlight color blend with glow effect
- Hover state intensifies to 50% for better interaction feedback
- Creates consistent visual language between tabs and content highlights
2025-10-14 10:32:31 +02:00
Gigi
a95f9b522b refactor(ui): simplify Me page tab labels
- Remove count numbers from all tabs (cleaner UI)
- Rename "Reading List" to "Bookmarks" (clearer naming)
- Keep tab names: Highlights, Bookmarks, Archive, Writings
- Reduces visual clutter and improves readability
2025-10-14 10:31:35 +02:00
Gigi
47d1335842 fix(ui): add background to Highlights tab for better contrast
- Add --color-bg-elevated background to active Highlights tab
- Improves contrast of yellow highlight color in light mode
- Creates visual separation while maintaining highlight color identity
- Keeps yellow text and border for consistent highlight theming
2025-10-14 10:30:27 +02:00
Gigi
168095e133 fix(ui): improve Highlights tab readability in light mode
- Use semantic text color (--color-text) for tab label in active state
- Keep highlight color for icon and bottom border as visual accent
- Ensures text is always readable regardless of theme
- Fixes contrast issues on /me page Highlights tab
2025-10-14 10:29:11 +02:00
Gigi
5c7b413a8d fix(theme): use consistent yellow-300 highlight color across all themes
- Revert to yellow-300 (#fde047) for all light and dark themes
- Use consistent Tailwind palette: yellow-300, orange-500, purple-600
- Previous darker colors were causing inconsistency with design system
- Ensures highlights use the same color values across all theme variants
2025-10-14 10:23:55 +02:00
Gigi
bca6458e44 fix(theme): improve highlight contrast in light themes
- Use darker yellow (yellow-400 instead of yellow-300) for better visibility
- Use darker orange (orange-600 instead of orange-500)
- Sepia theme uses even darker highlights (yellow-500, red-600)
- Ensures text and icons remain visible on highlighted text
- Applies to all light theme variants and system mode
2025-10-14 10:12:22 +02:00
Gigi
ebdfa3b5a3 fix(lint): replace 'any' types with proper type definitions
- Add DarkColorTheme and LightColorTheme type definitions
- Replace 'as any' with proper type assertions
- All eslint and TypeScript checks now pass
2025-10-14 10:10:57 +02:00
Gigi
17480dddbf fix(theme): improve text contrast in dark color themes
- Add text color definitions to all dark theme variants
- Ensure bright text (zinc-200) for readability on dark backgrounds
- Update --color-bg-subtle to be darker for better hierarchy
- Fixes low contrast issue where text was barely readable
2025-10-14 10:06:43 +02:00
Gigi
2a422fbeb9 fix(theme): use darker background for app body
- Change body background from --color-bg to --color-bg-subtle
- Creates visual depth and hierarchy between app bg and content
- Content panels now stand out more against the background
2025-10-14 10:05:10 +02:00
Gigi
22961ee479 fix(theme): update reading progress indicator to use theme colors
- Replace hard-coded dark background with --color-bg-elevated
- Use --color-border for progress track
- Use --color-primary for progress bar
- Use --color-text-muted for percentage text
- Indicator now adapts to light/dark themes
2025-10-14 10:02:43 +02:00
Gigi
18db905974 refactor(theme): rename labels from 'Colors' to 'Theme'
- Change 'Dark Colors' to 'Dark Theme'
- Change 'Light Colors' to 'Light Theme'
- More consistent and clearer labeling
2025-10-14 10:00:48 +02:00
Gigi
689963c041 refactor(theme): change default light theme to sepia
- Update default from paper-white to sepia for warmer reading
- Midnight remains default for dark mode
- Sepia provides warm, eye-friendly tones for light mode
2025-10-14 10:00:21 +02:00
Gigi
3f8869fd75 refactor(theme): show color swatches instead of text labels
- Replace text buttons with color swatches for theme selection
- Use actual background colors to preview each theme
- Add border for white swatch to make it visible
- Tooltips show theme names on hover
2025-10-14 09:58:47 +02:00
Gigi
129aced1a2 feat(theme): add color theme variants for light and dark modes
- Add darkColorTheme: black, midnight (default), charcoal
- Add lightColorTheme: paper-white (default), sepia, ivory
- Extend UserSettings with color theme fields
- Update ThemeSettings UI to show color options
- Add CSS variables for all color theme variants
- Sepia and Ivory have warm, reading-friendly palettes
- Black offers true black for OLED screens
- All color themes sync via Nostr (NIP-78)
2025-10-14 09:39:13 +02:00
Gigi
69febf4356 refactor(theme): remove localStorage, use only Nostr for persistence
- Remove localStorage.setItem/getItem from theme.ts
- Simplify early boot script to just default to system theme
- Theme now loads purely from NIP-78 settings
- Prevents race conditions between localStorage and Nostr settings
2025-10-14 09:34:54 +02:00
Gigi
65051c9c1f fix(theme): apply theme colors to body element
- Add background and text color to body
- Ensures page background changes with theme
2025-10-14 09:25:13 +02:00
Gigi
ba8229d464 refactor(css): update pull-to-refresh to use semantic tokens 2025-10-14 09:24:49 +02:00
Gigi
9251b017d4 refactor(css): migrate remaining components to semantic tokens
- Update icon-button.css, profile.css, me.css to use tokens
- Migrate reader.css to semantic colors for light theme
- Update toast.css with theme-aware colors
- All major UI components now support theme switching
2025-10-14 09:24:31 +02:00
Gigi
1ae76031f3 refactor(css): migrate cards/forms/layout to semantic tokens
- Replace hard-coded colors with CSS variables in cards.css
- Update forms.css, settings.css, modals.css with tokens
- Migrate sidebar.css and highlights.css to use semantic colors
- Update layout/app.css and base/global.css
- Enables proper light/dark theme switching
2025-10-14 09:13:42 +02:00
Gigi
994d834a0b feat(theme): add CSS variable tokens and theme classes
- Define semantic color tokens (--color-bg, --color-text, etc.)
- Add .theme-dark, .theme-light, .theme-system CSS classes
- Create theme.ts utility for theme application
- Add early boot theme script to prevent FOUC
- Support system preference with live updates
2025-10-14 09:11:38 +02:00
Gigi
67a4e17055 feat: add ants link to empty writings state for other users
- Update empty writings message for other users' profiles
- Show 'No articles written. You can find other stuff from this user using ants.'
- Link 'ants' to the ants.sh profile page for that user
- Keep original message for own profile
2025-10-14 01:42:29 +02:00
Gigi
1e82e3f240 fix: change empty state text color from red to gray
- Create new .explore-empty class with muted gray color (zinc-400)
- Keep .explore-error red for actual errors
- Update all empty state divs in Me.tsx to use .explore-empty
- Empty states (no highlights, no bookmarks, etc.) no longer appear as errors
2025-10-14 01:38:35 +02:00
Gigi
f973c75ff5 feat: match highlight comment color to highlight level color
- Remove hardcoded blue color from highlight comments
- Apply level-specific colors (mine/friends/nostrverse) to comment borders
- Use color-mix for subtle background tint matching highlight color
- Comment styling now respects user's highlight color settings
2025-10-14 01:36:59 +02:00
Gigi
28316a71c5 feat: open all profile links within app instead of external portals
- Update nostrUriResolver to return internal /p/:npub links for npub/nprofile
- Replace external profile links with React Router Link components
- Update ResolvedMention, LargeView, and CardView components
- Convert nprofile to npub before routing
- Keep note/nevent links as external (no internal viewer yet)
2025-10-14 01:32:28 +02:00
Gigi
cfc12e2d78 feat: add playful empty state message for other users' profiles
- Show 'You should shame them on nostr!' when viewing profiles with no highlights
- Keep original helpful message for own profile
- Conditional based on isOwnProfile flag
2025-10-14 01:29:54 +02:00
40 changed files with 1298 additions and 338 deletions

View File

@@ -7,6 +7,65 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
## [0.6.4] - 2025-10-14
### Added
- Color theme variants for light and dark modes
- Sepia, Classic (white/black), Rose, Sky, Mint, and Lavender themes
- Color swatches shown in theme selector instead of text labels
- CSS variable tokens and theme classes for consistent theming
- Playful empty state message for other users' profiles
- Profile links now open within app instead of external portals
### Changed
- Default light theme changed to sepia for better readability
- Theme setting labels renamed from 'Colors' to 'Theme'
- Highlight text now aligns properly with footer icons
- Increased spacing between highlight cards for better visual separation
- Increased bottom padding in highlight cards
- Simplified Me page tab labels for cleaner UI
- Highlight marker style applied to active Highlights tab
- All profile links open internally instead of via external Nostr portals
- Match highlight comment color to highlight level color
### Fixed
- Consistent yellow-300 highlight color across all themes
- Highlight contrast improved in light themes
- Text contrast improved in dark color themes
- Darker background for app body in dark themes
- Reading progress indicator now uses theme colors
- Highlights tab readability improved in light mode with proper background
- Empty state text color changed from red to gray for better aesthetics
- Replaced 'any' types with proper type definitions for better type safety
### Refactored
- Migrated entire codebase to semantic token system
- Pull-to-refresh components updated to use semantic tokens
- Cards, forms, and layout components migrated to semantic tokens
- All remaining components converted to semantic token usage
- Removed localStorage for theme persistence, using only Nostr (NIP-78)
- Theme colors applied to body element for consistent theming
## [0.6.3] - 2025-10-14
### Added
- Ants link to empty writings state for other users
### Changed
- Empty state text color from red to gray
### Fixed
- Match highlight comment color to highlight level color
- Open all profile links within app instead of external portals
- Playful empty state message for other users' profiles
## [0.6.2] - 2025-01-27
### Added
@@ -1096,7 +1155,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Optimize relay usage following applesauce-relay best practices
- Use applesauce-react event models for better profile handling
[Unreleased]: https://github.com/dergigi/boris/compare/v0.6.2...HEAD
[Unreleased]: https://github.com/dergigi/boris/compare/v0.6.4...HEAD
[0.6.4]: https://github.com/dergigi/boris/compare/v0.6.3...v0.6.4
[0.6.3]: https://github.com/dergigi/boris/compare/v0.6.2...v0.6.3
[0.6.2]: https://github.com/dergigi/boris/compare/v0.6.1...v0.6.2
[0.6.1]: https://github.com/dergigi/boris/compare/v0.6.0...v0.6.1
[0.6.0]: https://github.com/dergigi/boris/compare/v0.5.7...v0.6.0

View File

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

17
package-lock.json generated
View File

@@ -1,14 +1,15 @@
{
"name": "boris",
"version": "0.5.7",
"version": "0.6.5",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "boris",
"version": "0.5.7",
"version": "0.6.5",
"dependencies": {
"@fortawesome/fontawesome-svg-core": "^7.1.0",
"@fortawesome/free-regular-svg-icons": "^7.1.0",
"@fortawesome/free-solid-svg-icons": "^7.1.0",
"@fortawesome/react-fontawesome": "^3.0.2",
"@treeee/youtube-caption-extractor": "^1.5.5",
@@ -2263,6 +2264,18 @@
"node": ">=6"
}
},
"node_modules/@fortawesome/free-regular-svg-icons": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-7.1.0.tgz",
"integrity": "sha512-0e2fdEyB4AR+e6kU4yxwA/MonnYcw/CsMEP9lH82ORFi9svA6/RhDyhxIv5mlJaldmaHLLYVTb+3iEr+PDSZuQ==",
"license": "(CC-BY-4.0 AND MIT)",
"dependencies": {
"@fortawesome/fontawesome-common-types": "7.1.0"
},
"engines": {
"node": ">=6"
}
},
"node_modules/@fortawesome/free-solid-svg-icons": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-7.1.0.tgz",

View File

@@ -1,6 +1,6 @@
{
"name": "boris",
"version": "0.6.3",
"version": "0.6.5",
"description": "A minimal nostr client for bookmark management",
"homepage": "https://read.withboris.com/",
"type": "module",
@@ -12,6 +12,7 @@
},
"dependencies": {
"@fortawesome/fontawesome-svg-core": "^7.1.0",
"@fortawesome/free-regular-svg-icons": "^7.1.0",
"@fortawesome/free-solid-svg-icons": "^7.1.0",
"@fortawesome/react-fontawesome": "^3.0.2",
"@treeee/youtube-caption-extractor": "^1.5.5",

View File

@@ -70,6 +70,15 @@ function AppRoutes({
/>
}
/>
<Route
path="/explore/writings"
element={
<Bookmarks
relayPool={relayPool}
onLogout={handleLogout}
/>
}
/>
<Route
path="/me"
element={<Navigate to="/me/highlights" replace />}

View File

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

View File

@@ -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 && (

View File

@@ -31,15 +31,21 @@ const Bookmarks: React.FC<BookmarksProps> = ({ relayPool, onLogout }) => {
const navigate = useNavigate()
const previousLocationRef = useRef<string>()
// Check for highlight navigation state
const navigationState = location.state as { highlightId?: string; openHighlights?: boolean } | null
const externalUrl = location.pathname.startsWith('/r/')
? decodeURIComponent(location.pathname.slice(3))
: undefined
const showSettings = location.pathname === '/settings'
const showExplore = location.pathname === '/explore'
const showExplore = location.pathname.startsWith('/explore')
const showMe = location.pathname.startsWith('/me')
const showProfile = location.pathname.startsWith('/p/')
// Extract tab from explore routes
const exploreTab = location.pathname === '/explore/writings' ? 'writings' : 'highlights'
// Extract tab from me routes
const meTab = location.pathname === '/me' ? 'highlights' :
location.pathname === '/me/highlights' ? 'highlights' :
@@ -125,6 +131,19 @@ const Bookmarks: React.FC<BookmarksProps> = ({ relayPool, onLogout }) => {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [location.pathname])
// Handle highlight navigation from explore page
useEffect(() => {
if (navigationState?.highlightId && navigationState?.openHighlights) {
// Open the highlights sidebar
setIsHighlightsCollapsed(false)
// Select the highlight (scroll happens automatically in useHighlightInteractions)
setSelectedHighlightId(navigationState.highlightId)
// Clear the state after handling to avoid re-triggering
navigate(location.pathname, { replace: true, state: {} })
}
}, [navigationState, setIsHighlightsCollapsed, setSelectedHighlightId, navigate, location.pathname])
const {
bookmarks,
bookmarksLoading,
@@ -286,7 +305,7 @@ const Bookmarks: React.FC<BookmarksProps> = ({ relayPool, onLogout }) => {
onCreateHighlight={handleCreateHighlight}
hasActiveAccount={!!(activeAccount && relayPool)}
explore={showExplore ? (
relayPool ? <Explore relayPool={relayPool} /> : null
relayPool ? <Explore relayPool={relayPool} activeTab={exploreTab} /> : null
) : undefined}
me={showMe ? (
relayPool ? <Me relayPool={relayPool} activeTab={meTab} /> : null

View File

@@ -1,30 +1,49 @@
import React, { useState, useEffect, useRef } from 'react'
import React, { useState, useEffect, useRef, useMemo } from 'react'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faSpinner, faExclamationCircle, faNewspaper } from '@fortawesome/free-solid-svg-icons'
import { faSpinner, faExclamationCircle, faNewspaper, faPenToSquare, faHighlighter } from '@fortawesome/free-solid-svg-icons'
import { Hooks } from 'applesauce-react'
import { RelayPool } from 'applesauce-relay'
import { nip19 } from 'nostr-tools'
import { useNavigate } from 'react-router-dom'
import { fetchContacts } from '../services/contactService'
import { fetchBlogPostsFromAuthors, BlogPostPreview } from '../services/exploreService'
import { fetchHighlightsFromAuthors } from '../services/highlightService'
import { Highlight } from '../types/highlights'
import BlogPostCard from './BlogPostCard'
import { getCachedPosts, upsertCachedPost, setCachedPosts } from '../services/exploreCache'
import { HighlightItem } from './HighlightItem'
import { getCachedPosts, upsertCachedPost, setCachedPosts, getCachedHighlights, upsertCachedHighlight, setCachedHighlights } from '../services/exploreCache'
import { usePullToRefresh } from '../hooks/usePullToRefresh'
import PullToRefreshIndicator from './PullToRefreshIndicator'
import { classifyHighlights } from '../utils/highlightClassification'
interface ExploreProps {
relayPool: RelayPool
activeTab?: TabType
}
const Explore: React.FC<ExploreProps> = ({ relayPool }) => {
type TabType = 'writings' | 'highlights'
const Explore: React.FC<ExploreProps> = ({ relayPool, activeTab: propActiveTab }) => {
const activeAccount = Hooks.useActiveAccount()
const navigate = useNavigate()
const [activeTab, setActiveTab] = useState<TabType>(propActiveTab || 'highlights')
const [blogPosts, setBlogPosts] = useState<BlogPostPreview[]>([])
const [highlights, setHighlights] = useState<Highlight[]>([])
const [followedPubkeys, setFollowedPubkeys] = useState<Set<string>>(new Set())
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const exploreContainerRef = useRef<HTMLDivElement>(null)
const [refreshTrigger, setRefreshTrigger] = useState(0)
// Update local state when prop changes
useEffect(() => {
const loadBlogPosts = async () => {
if (propActiveTab) {
setActiveTab(propActiveTab)
}
}, [propActiveTab])
useEffect(() => {
const loadData = async () => {
if (!activeAccount) {
setError('Please log in to explore content from your friends')
setLoading(false)
@@ -32,14 +51,18 @@ const Explore: React.FC<ExploreProps> = ({ relayPool }) => {
}
try {
// show spinner but keep existing posts
// show spinner but keep existing data
setLoading(true)
setError(null)
// Seed from in-memory cache if available to avoid empty flash
const cached = getCachedPosts(activeAccount.pubkey)
if (cached && cached.length > 0 && blogPosts.length === 0) {
setBlogPosts(cached)
const cachedPosts = getCachedPosts(activeAccount.pubkey)
if (cachedPosts && cachedPosts.length > 0 && blogPosts.length === 0) {
setBlogPosts(cachedPosts)
}
const cachedHighlights = getCachedHighlights(activeAccount.pubkey)
if (cachedHighlights && cachedHighlights.length > 0 && highlights.length === 0) {
setHighlights(cachedHighlights)
}
// Fetch the user's contacts (friends)
@@ -47,15 +70,19 @@ const Explore: React.FC<ExploreProps> = ({ relayPool }) => {
relayPool,
activeAccount.pubkey,
(partial) => {
// When local contacts are available, kick off early posts fetch
// Store followed pubkeys for highlight classification
setFollowedPubkeys(partial)
// When local contacts are available, kick off early fetch
if (partial.size > 0) {
const relayUrls = Array.from(relayPool.relays.values()).map(relay => relay.url)
const partialArray = Array.from(partial)
// Fetch blog posts
fetchBlogPostsFromAuthors(
relayPool,
Array.from(partial),
partialArray,
relayUrls,
(post) => {
// merge into UI and cache as we stream
setBlogPosts((prev) => {
const exists = prev.some(p => p.event.id === post.event.id)
if (exists) return prev
@@ -69,7 +96,6 @@ const Explore: React.FC<ExploreProps> = ({ relayPool }) => {
setCachedPosts(activeAccount.pubkey, upsertCachedPost(activeAccount.pubkey, post))
}
).then((all) => {
// Ensure union of streamed + final is displayed
setBlogPosts((prev) => {
const byId = new Map(prev.map(p => [p.event.id, p]))
for (const post of all) byId.set(post.event.id, post)
@@ -82,22 +108,52 @@ const Explore: React.FC<ExploreProps> = ({ relayPool }) => {
return merged
})
})
// Fetch highlights
fetchHighlightsFromAuthors(
relayPool,
partialArray,
(highlight) => {
setHighlights((prev) => {
const exists = prev.some(h => h.id === highlight.id)
if (exists) return prev
const next = [...prev, highlight]
return next.sort((a, b) => b.created_at - a.created_at)
})
setCachedHighlights(activeAccount.pubkey, upsertCachedHighlight(activeAccount.pubkey, highlight))
}
).then((all) => {
setHighlights((prev) => {
const byId = new Map(prev.map(h => [h.id, h]))
for (const highlight of all) byId.set(highlight.id, highlight)
const merged = Array.from(byId.values()).sort((a, b) => b.created_at - a.created_at)
setCachedHighlights(activeAccount.pubkey, merged)
return merged
})
})
}
}
)
if (contacts.size === 0) {
setError('You are not following anyone yet. Follow some people to see their blog posts!')
setError('You are not following anyone yet. Follow some people to see their content!')
setLoading(false)
return
}
// Store final followed pubkeys
setFollowedPubkeys(contacts)
// After full contacts, do a final pass for completeness
const relayUrls = Array.from(relayPool.relays.values()).map(relay => relay.url)
const posts = await fetchBlogPostsFromAuthors(relayPool, Array.from(contacts), relayUrls)
const contactsArray = Array.from(contacts)
const [posts, userHighlights] = await Promise.all([
fetchBlogPostsFromAuthors(relayPool, contactsArray, relayUrls),
fetchHighlightsFromAuthors(relayPool, contactsArray)
])
if (posts.length === 0) {
setError('No blog posts found from your friends yet')
if (posts.length === 0 && userHighlights.length === 0) {
setError('No content found from your friends yet')
}
setBlogPosts((prev) => {
@@ -111,16 +167,24 @@ const Explore: React.FC<ExploreProps> = ({ relayPool }) => {
setCachedPosts(activeAccount.pubkey, merged)
return merged
})
setHighlights((prev) => {
const byId = new Map(prev.map(h => [h.id, h]))
for (const highlight of userHighlights) byId.set(highlight.id, highlight)
const merged = Array.from(byId.values()).sort((a, b) => b.created_at - a.created_at)
setCachedHighlights(activeAccount.pubkey, merged)
return merged
})
} catch (err) {
console.error('Failed to load blog posts:', err)
setError('Failed to load blog posts. Please try again.')
console.error('Failed to load data:', err)
setError('Failed to load content. Please try again.')
} finally {
setLoading(false)
}
}
loadBlogPosts()
}, [relayPool, activeAccount, blogPosts.length, refreshTrigger])
loadData()
}, [relayPool, activeAccount, blogPosts.length, highlights.length, refreshTrigger])
// Pull-to-refresh
const pullToRefreshState = usePullToRefresh(exploreContainerRef, {
@@ -144,6 +208,96 @@ const Explore: React.FC<ExploreProps> = ({ relayPool }) => {
return `/a/${naddr}`
}
const handleHighlightClick = (highlightId: string) => {
const highlight = highlights.find(h => h.id === highlightId)
if (!highlight) return
// For nostr-native articles
if (highlight.eventReference) {
// Convert eventReference to naddr
if (highlight.eventReference.includes(':')) {
const parts = highlight.eventReference.split(':')
const kind = parseInt(parts[0])
const pubkey = parts[1]
const identifier = parts[2] || ''
const naddr = nip19.naddrEncode({
kind,
pubkey,
identifier
})
navigate(`/a/${naddr}`, { state: { highlightId, openHighlights: true } })
} else {
// Already an naddr
navigate(`/a/${highlight.eventReference}`, { state: { highlightId, openHighlights: true } })
}
}
// For web URLs
else if (highlight.urlReference) {
navigate(`/r/${encodeURIComponent(highlight.urlReference)}`, { state: { highlightId, openHighlights: true } })
}
}
// Classify highlights with levels based on user context
const classifiedHighlights = useMemo(() => {
return classifyHighlights(highlights, activeAccount?.pubkey, followedPubkeys)
}, [highlights, activeAccount?.pubkey, followedPubkeys])
const renderTabContent = () => {
switch (activeTab) {
case 'writings':
return blogPosts.length === 0 ? (
<div className="explore-empty" style={{ gridColumn: '1/-1', textAlign: 'center', color: 'var(--text-secondary)' }}>
<p>No blog posts found yet.</p>
</div>
) : (
<div className="explore-grid">
{blogPosts.map((post) => (
<BlogPostCard
key={`${post.author}:${post.event.tags.find(t => t[0] === 'd')?.[1]}`}
post={post}
href={getPostUrl(post)}
/>
))}
</div>
)
case 'highlights':
return classifiedHighlights.length === 0 ? (
<div className="explore-empty" style={{ gridColumn: '1/-1', textAlign: 'center', color: 'var(--text-secondary)' }}>
<p>No highlights yet. Your friends should start highlighting content!</p>
</div>
) : (
<div className="explore-grid">
{classifiedHighlights.map((highlight) => (
<HighlightItem
key={highlight.id}
highlight={highlight}
relayPool={relayPool}
onHighlightClick={handleHighlightClick}
/>
))}
</div>
)
default:
return null
}
}
// Only show full loading screen if we don't have any data yet
const hasData = highlights.length > 0 || blogPosts.length > 0
if (loading && !hasData) {
return (
<div className="explore-container">
<div className="explore-loading">
<FontAwesomeIcon icon={faSpinner} spin size="2x" />
</div>
</div>
)
}
if (error) {
return (
<div className="explore-container">
@@ -172,28 +326,36 @@ const Explore: React.FC<ExploreProps> = ({ relayPool }) => {
Explore
</h1>
<p className="explore-subtitle">
Discover blog posts from your friends on Nostr
Discover highlights and blog posts from your friends and others
</p>
</div>
{loading && (
<div className="explore-loading" style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', padding: '0.5rem 0' }}>
<FontAwesomeIcon icon={faSpinner} spin />
</div>
)}
<div className="explore-grid">
{blogPosts.map((post) => (
<BlogPostCard
key={`${post.author}:${post.event.tags.find(t => t[0] === 'd')?.[1]}`}
post={post}
href={getPostUrl(post)}
/>
))}
{!loading && blogPosts.length === 0 && (
<div className="explore-empty" style={{ gridColumn: '1/-1', textAlign: 'center', color: 'var(--text-secondary)' }}>
<p>No blog posts found yet.</p>
{loading && hasData && (
<div className="explore-loading" style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', padding: '0.5rem 0' }}>
<FontAwesomeIcon icon={faSpinner} spin />
</div>
)}
<div className="me-tabs">
<button
className={`me-tab ${activeTab === 'highlights' ? 'active' : ''}`}
data-tab="highlights"
onClick={() => navigate('/explore')}
>
<FontAwesomeIcon icon={faHighlighter} />
<span className="tab-label">Highlights</span>
</button>
<button
className={`me-tab ${activeTab === 'writings' ? 'active' : ''}`}
data-tab="writings"
onClick={() => navigate('/explore/writings')}
>
<FontAwesomeIcon icon={faPenToSquare} />
<span className="tab-label">Writings</span>
</button>
</div>
</div>
{renderTabContent()}
</div>
)
}

View File

@@ -0,0 +1,104 @@
import React, { useState, useEffect, useMemo } from 'react'
import { RelayPool } from 'applesauce-relay'
import { useEventModel } from 'applesauce-react/hooks'
import { Models } from 'applesauce-core'
import { nip19 } from 'nostr-tools'
import { fetchArticleTitle } from '../services/articleTitleResolver'
import { Highlight } from '../types/highlights'
interface HighlightCitationProps {
highlight: Highlight
relayPool?: RelayPool | null
}
export const HighlightCitation: React.FC<HighlightCitationProps> = ({
highlight,
relayPool
}) => {
const [articleTitle, setArticleTitle] = useState<string>()
// Extract author pubkey from p tag directly
const authorPubkey = useMemo(() => {
// First try the extracted author from highlight.author
if (highlight.author) {
return highlight.author
}
// Fallback: extract directly from p tag
const pTag = highlight.tags.find(t => t[0] === 'p')
if (pTag && pTag[1]) {
console.log('📝 Found author from p tag:', pTag[1])
return pTag[1]
}
return undefined
}, [highlight.author, highlight.tags])
const authorProfile = useEventModel(Models.ProfileModel, authorPubkey ? [authorPubkey] : null)
useEffect(() => {
if (!highlight.eventReference || !relayPool) {
return
}
const loadTitle = async () => {
try {
if (!highlight.eventReference) return
// Convert eventReference to naddr if needed
let naddr: string
if (highlight.eventReference.includes(':')) {
const parts = highlight.eventReference.split(':')
const kind = parseInt(parts[0])
const pubkey = parts[1]
const identifier = parts[2] || ''
naddr = nip19.naddrEncode({
kind,
pubkey,
identifier
})
} else {
naddr = highlight.eventReference
}
const title = await fetchArticleTitle(relayPool, naddr)
if (title) {
setArticleTitle(title)
}
} catch (error) {
console.error('Failed to load article title:', error)
}
}
loadTitle()
}, [highlight.eventReference, relayPool])
const authorName = authorProfile?.name || authorProfile?.display_name
// For nostr-native content with article reference
if (highlight.eventReference && (authorName || articleTitle)) {
return (
<div className="highlight-citation">
{authorName || 'Unknown'}{articleTitle ? `, ${articleTitle}` : ''}
</div>
)
}
// For web URLs
if (highlight.urlReference) {
try {
const url = new URL(highlight.urlReference)
return (
<div className="highlight-citation">
{url.hostname}
</div>
)
} catch {
return null
}
}
return null
}

View File

@@ -1,6 +1,7 @@
import React, { useEffect, useRef, useState } from 'react'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faQuoteLeft, faExternalLinkAlt, faPlane, faSpinner, faServer, faTrash, faEllipsisH, faMobileAlt } from '@fortawesome/free-solid-svg-icons'
import { faQuoteLeft, faExternalLinkAlt, faPlane, faSpinner, faHighlighter, faTrash, faEllipsisH, faMobileAlt } from '@fortawesome/free-solid-svg-icons'
import { faComments } from '@fortawesome/free-regular-svg-icons'
import { Highlight } from '../types/highlights'
import { useEventModel } from 'applesauce-react/hooks'
import { Models, IEventStore } from 'applesauce-core'
@@ -15,6 +16,7 @@ import { createDeletionRequest } from '../services/deletionService'
import ConfirmDialog from './ConfirmDialog'
import { getNostrUrl } from '../config/nostrGateways'
import CompactButton from './CompactButton'
import { HighlightCitation } from './HighlightCitation'
interface HighlightWithLevel extends Highlight {
level?: 'mine' | 'friends' | 'nostrverse'
@@ -208,13 +210,13 @@ export const HighlightItem: React.FC<HighlightItemProps> = ({
// Always show relay list, use plane icon for local-only
const isLocalOrOffline = highlight.isLocalOnly || showOfflineIndicator
// Show server icon with relay info if available
// Show highlighter icon with relay info if available
if (highlight.publishedRelays && highlight.publishedRelays.length > 0) {
const relayNames = highlight.publishedRelays.map(url =>
url.replace(/^wss?:\/\//, '').replace(/\/$/, '')
)
return {
icon: isLocalOrOffline ? faPlane : faServer,
icon: isLocalOrOffline ? faPlane : faHighlighter,
tooltip: relayNames.join('\n'),
spin: false
}
@@ -225,7 +227,7 @@ export const HighlightItem: React.FC<HighlightItemProps> = ({
url.replace(/^wss?:\/\//, '').replace(/\/$/, '')
)
return {
icon: faServer,
icon: faHighlighter,
tooltip: relayNames.join('\n'),
spin: false
}
@@ -236,7 +238,7 @@ export const HighlightItem: React.FC<HighlightItemProps> = ({
url.replace(/^wss?:\/\//, '').replace(/\/$/, '')
)
return {
icon: faServer,
icon: faHighlighter,
tooltip: relayNames.join('\n'),
spin: false
}
@@ -318,7 +320,10 @@ export const HighlightItem: React.FC<HighlightItemProps> = ({
<CompactButton
className="highlight-timestamp"
title={new Date(highlight.created_at * 1000).toLocaleString()}
onClick={(e) => e.stopPropagation()}
onClick={(e) => {
e.stopPropagation()
window.location.href = highlightLinks.native
}}
>
{formatDateCompact(highlight.created_at)}
</CompactButton>
@@ -338,8 +343,14 @@ export const HighlightItem: React.FC<HighlightItemProps> = ({
{highlight.content}
</blockquote>
<HighlightCitation
highlight={highlight}
relayPool={relayPool}
/>
{highlight.comment && (
<div className="highlight-comment">
<FontAwesomeIcon icon={faComments} flip="horizontal" className="highlight-comment-icon" />
{highlight.comment}
</div>
)}

View File

@@ -23,6 +23,7 @@ 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
@@ -220,8 +221,12 @@ const Me: React.FC<MeProps> = ({ relayPool, activeTab: propActiveTab, pubkey: pr
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">
@@ -238,7 +243,7 @@ const Me: React.FC<MeProps> = ({ relayPool, activeTab: propActiveTab, pubkey: pr
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>
) : (
@@ -289,7 +294,7 @@ const Me: React.FC<MeProps> = ({ relayPool, activeTab: propActiveTab, pubkey: pr
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>
) : (
@@ -306,8 +311,25 @@ const Me: React.FC<MeProps> = ({ relayPool, activeTab: propActiveTab, pubkey: pr
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">
@@ -354,7 +376,6 @@ const Me: React.FC<MeProps> = ({ relayPool, activeTab: propActiveTab, pubkey: pr
>
<FontAwesomeIcon icon={faHighlighter} />
<span className="tab-label">Highlights</span>
<span className="tab-count">({highlights.length})</span>
</button>
{isOwnProfile && (
<>
@@ -364,8 +385,7 @@ const Me: React.FC<MeProps> = ({ relayPool, activeTab: propActiveTab, pubkey: pr
onClick={() => navigate('/me/reading-list')}
>
<FontAwesomeIcon icon={faBookmark} />
<span className="tab-label">Reading List</span>
<span className="tab-count">({allIndividualBookmarks.length})</span>
<span className="tab-label">Bookmarks</span>
</button>
<button
className={`me-tab ${activeTab === 'archive' ? 'active' : ''}`}
@@ -374,7 +394,6 @@ const Me: React.FC<MeProps> = ({ relayPool, activeTab: propActiveTab, pubkey: pr
>
<FontAwesomeIcon icon={faBooks} />
<span className="tab-label">Archive</span>
<span className="tab-count">({readArticles.length})</span>
</button>
</>
)}
@@ -385,7 +404,6 @@ const Me: React.FC<MeProps> = ({ relayPool, activeTab: propActiveTab, pubkey: pr
>
<FontAwesomeIcon icon={faPenToSquare} />
<span className="tab-label">Writings</span>
<span className="tab-count">({writings.length})</span>
</button>
</div>
</div>

View File

@@ -38,7 +38,7 @@ const ReaderHeader: React.FC<ReaderHeaderProps> = ({
const isLongSummary = summary && summary.length > 150
// Determine the dominant highlight color based on visibility and priority
const highlightIndicatorStyles = useMemo(() => {
const getHighlightIndicatorStyles = useMemo(() => (isOverlay: boolean) => {
if (!highlights.length) return undefined
// Count highlights by level that are visible
@@ -65,7 +65,8 @@ const ReaderHeader: React.FC<ReaderHeaderProps> = ({
return {
backgroundColor: `rgba(${rgb}, 0.1)`,
borderColor: `rgba(${rgb}, 0.3)`,
color: '#fff'
// Only force white color in overlay context, otherwise let CSS handle it
...(isOverlay && { color: '#fff' })
}
}, [highlights, highlightVisibility, settings])
@@ -93,7 +94,7 @@ const ReaderHeader: React.FC<ReaderHeaderProps> = ({
{hasHighlights && (
<div
className="highlight-indicator"
style={highlightIndicatorStyles}
style={getHighlightIndicatorStyles(true)}
>
<FontAwesomeIcon icon={faHighlighter} />
<span>{highlightCount} highlight{highlightCount !== 1 ? 's' : ''}</span>
@@ -133,7 +134,7 @@ const ReaderHeader: React.FC<ReaderHeaderProps> = ({
{hasHighlights && (
<div
className="highlight-indicator"
style={highlightIndicatorStyles}
style={getHighlightIndicatorStyles(false)}
>
<FontAwesomeIcon icon={faHighlighter} />
<span>{highlightCount} highlight{highlightCount !== 1 ? 's' : ''}</span>

View File

@@ -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>
)}

View File

@@ -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>
)
}

View File

@@ -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} />

View 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

View File

@@ -58,12 +58,15 @@ export const useHighlightInteractions = ({
}
}, [onHighlightClick, contentVersion])
// Scroll to selected highlight
// Scroll to selected highlight with retry mechanism
useEffect(() => {
if (!selectedHighlightId || !contentRef.current) return
// Use a small delay to ensure DOM is updated
const timeoutId = setTimeout(() => {
let attempts = 0
const maxAttempts = 20 // Try for up to 2 seconds
const retryDelay = 100
const tryScroll = () => {
if (!contentRef.current) return
const markElement = contentRef.current.querySelector(`mark[data-highlight-id="${selectedHighlightId}"]`)
@@ -76,10 +79,16 @@ export const useHighlightInteractions = ({
htmlElement.classList.add('highlight-pulse')
setTimeout(() => htmlElement.classList.remove('highlight-pulse'), 1500)
}, 500)
} else if (attempts < maxAttempts) {
attempts++
setTimeout(tryScroll, retryDelay)
} else {
console.warn('Could not find mark element for highlight:', selectedHighlightId)
console.warn('Could not find mark element for highlight after', maxAttempts, 'attempts:', selectedHighlightId)
}
}, 100)
}
// Start trying after a small initial delay
const timeoutId = setTimeout(tryScroll, 100)
return () => clearTimeout(timeoutId)
}, [selectedHighlightId, contentVersion])

View File

@@ -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') {

View File

@@ -1,4 +1,5 @@
import { NostrEvent } from 'nostr-tools'
import { Highlight } from '../types/highlights'
export interface CachedBlogPostPreview {
event: NostrEvent
@@ -11,6 +12,7 @@ export interface CachedBlogPostPreview {
type CacheValue = {
posts: CachedBlogPostPreview[]
highlights: Highlight[]
timestamp: number
}
@@ -22,8 +24,28 @@ export function getCachedPosts(pubkey: string): CachedBlogPostPreview[] | null {
return entry.posts
}
export function getCachedHighlights(pubkey: string): Highlight[] | null {
const entry = exploreCache.get(pubkey)
if (!entry) return null
return entry.highlights
}
export function setCachedPosts(pubkey: string, posts: CachedBlogPostPreview[]): void {
exploreCache.set(pubkey, { posts, timestamp: Date.now() })
const current = exploreCache.get(pubkey)
exploreCache.set(pubkey, {
posts,
highlights: current?.highlights || [],
timestamp: Date.now()
})
}
export function setCachedHighlights(pubkey: string, highlights: Highlight[]): void {
const current = exploreCache.get(pubkey)
exploreCache.set(pubkey, {
posts: current?.posts || [],
highlights,
timestamp: Date.now()
})
}
export function upsertCachedPost(pubkey: string, post: CachedBlogPostPreview): CachedBlogPostPreview[] {
@@ -39,4 +61,13 @@ export function upsertCachedPost(pubkey: string, post: CachedBlogPostPreview): C
return merged
}
export function upsertCachedHighlight(pubkey: string, highlight: Highlight): Highlight[] {
const current = exploreCache.get(pubkey)?.highlights || []
const byId = new Map(current.map(h => [h.id, h]))
byId.set(highlight.id, highlight)
const merged = Array.from(byId.values()).sort((a, b) => b.created_at - a.created_at)
setCachedHighlights(pubkey, merged)
return merged
}

View File

@@ -1,5 +1,5 @@
export * from './highlights/fetchForArticle'
export * from './highlights/fetchForUrl'
export * from './highlights/fetchByAuthor'
export * from './highlights/fetchFromAuthors'

View File

@@ -0,0 +1,79 @@
import { RelayPool, completeOnEose, onlyEvents } from 'applesauce-relay'
import { lastValueFrom, merge, Observable, takeUntil, timer, tap, toArray } from 'rxjs'
import { NostrEvent } from 'nostr-tools'
import { Highlight } from '../../types/highlights'
import { prioritizeLocalRelays, partitionRelays } from '../../utils/helpers'
import { eventToHighlight, dedupeHighlights, sortHighlights } from '../highlightEventProcessor'
/**
* Fetches highlights (kind:9802) from a list of pubkeys (friends)
* @param relayPool - The relay pool to query
* @param pubkeys - Array of pubkeys to fetch highlights from
* @param onHighlight - Optional callback for streaming highlights as they arrive
* @returns Array of highlights
*/
export const fetchHighlightsFromAuthors = async (
relayPool: RelayPool,
pubkeys: string[],
onHighlight?: (highlight: Highlight) => void
): Promise<Highlight[]> => {
try {
if (pubkeys.length === 0) {
console.log('⚠️ No pubkeys to fetch highlights from')
return []
}
console.log('💡 Fetching highlights (kind 9802) from', pubkeys.length, 'authors')
const relayUrls = Array.from(relayPool.relays.values()).map(relay => relay.url)
const prioritized = prioritizeLocalRelays(relayUrls)
const { local: localRelays, remote: remoteRelays } = partitionRelays(prioritized)
const seenIds = new Set<string>()
const local$ = localRelays.length > 0
? relayPool
.req(localRelays, { kinds: [9802], authors: pubkeys, limit: 200 })
.pipe(
onlyEvents(),
tap((event: NostrEvent) => {
if (!seenIds.has(event.id)) {
seenIds.add(event.id)
if (onHighlight) onHighlight(eventToHighlight(event))
}
}),
completeOnEose(),
takeUntil(timer(1200))
)
: new Observable<NostrEvent>((sub) => sub.complete())
const remote$ = remoteRelays.length > 0
? relayPool
.req(remoteRelays, { kinds: [9802], authors: pubkeys, limit: 200 })
.pipe(
onlyEvents(),
tap((event: NostrEvent) => {
if (!seenIds.has(event.id)) {
seenIds.add(event.id)
if (onHighlight) onHighlight(eventToHighlight(event))
}
}),
completeOnEose(),
takeUntil(timer(6000))
)
: new Observable<NostrEvent>((sub) => sub.complete())
const rawEvents: NostrEvent[] = await lastValueFrom(merge(local$, remote$).pipe(toArray()))
const uniqueEvents = dedupeHighlights(rawEvents)
const highlights = uniqueEvents.map(eventToHighlight)
console.log('💡 Processed', highlights.length, 'unique highlights')
return sortHighlights(highlights)
} catch (error) {
console.error('Failed to fetch highlights from authors:', error)
return []
}
}

View File

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

View File

@@ -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);
}

View File

@@ -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;
}
}

View File

@@ -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,76 @@
.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-header .me-tabs { text-align: left; margin-top: 2rem; width: 100%; max-width: 100%; justify-content: flex-start; }
.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 +99,3 @@
.blog-post-card-content { padding: 1rem; }
}

View File

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

View File

@@ -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); }
}

View File

@@ -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;
}

View File

@@ -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; }

View File

@@ -1,14 +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%; transition: all 0.2s ease; }
.author-card-clickable:hover { border-color: rgb(99 102 241); /* indigo-500 */ background: rgb(30 30 33); /* slightly lighter */ transform: translateY(-1px); }
.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: rgb(39 39 42); /* zinc-800 */ display: flex; align-items: center; justify-content: center; color: rgb(113 113 122); /* zinc-500 */ }
.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 {
@@ -28,4 +28,3 @@
.author-card-bio { font-size: 0.85rem; -webkit-line-clamp: 2; }
}

View File

@@ -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);

View File

@@ -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-text); }
.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 */

View File

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

View File

@@ -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; } }

View File

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

View File

@@ -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,20 @@
.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-citation { margin-left: 1.25rem; font-size: 0.8rem; color: var(--color-text-secondary); font-style: normal; padding-top: 0.25rem; }
.highlight-comment { margin-top: 0.5rem; padding: 0.75rem; border-radius: 4px; font-size: 0.875rem; color: var(--color-text); line-height: 1.5; display: flex; gap: 0.5rem; align-items: flex-start; }
.highlight-comment-icon { flex-shrink: 0; margin-top: 0.125rem; }
.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 comment icons */
.highlight-item.level-mine .highlight-comment-icon { color: var(--highlight-color-mine, #ffff00); }
.highlight-item.level-friends .highlight-comment-icon { color: var(--highlight-color-friends, #f97316); }
.highlight-item.level-nostrverse .highlight-comment-icon { 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 +150,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; }

View File

@@ -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 */ }

View File

@@ -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
View 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 })
}