Compare commits

..

35 Commits

Author SHA1 Message Date
Gigi
d313c71e24 chore: bump version to 0.10.25 2025-10-25 01:59:25 +02:00
Gigi
903b4a4ec1 Merge pull request #28 from dergigi/mid-size-cards
feat: redesign medium-sized bookmark cards with compact layout and improved UX
2025-10-25 01:58:56 +02:00
Gigi
a511b25b87 fix: correct TypeScript error in content type icon logic
- Change 'document' case to 'article' to match valid UrlType
- Fix TypeScript compilation error for invalid UrlType comparison
- Maintain proper type safety while preserving icon functionality
- All linting and type checks now passing
2025-10-25 01:57:57 +02:00
Gigi
e920cf9477 feat: make image placeholder reflect bookmark type
- Add content type icon logic to CardView component
- Import appropriate FontAwesome icons for different content types
- Replace generic link icon with type-specific icons:
  - Articles: newspaper icon
  - Videos: play button icon
  - Images: camera icon
  - Documents: file lines icon
  - Web links: globe icon
  - Notes: sticky note icon
- Improve visual context and user experience with meaningful placeholders
2025-10-25 01:56:31 +02:00
Gigi
708a1bfd54 fix: ensure consistent reading progress bar thickness in large cards
- Add minHeight property to ReadingProgressBar container and inner div
- Ensure empty progress bar maintains same 3px height as filled progress bar
- Fix visual consistency between empty and filled reading progress states
- Maintain proper visual separator thickness in large card view
2025-10-25 01:55:37 +02:00
Gigi
51842f55bf fix: resolve linting issues in CardView component
- Remove unused onSelectUrl parameter from CardView destructuring
- Fix ESLint no-unused-vars error
- Maintain code quality and linting standards
- All linting issues resolved, type checks passing
2025-10-25 01:54:59 +02:00
Gigi
52991f8e20 fix: eliminate 0 artefacts in compact view conditional rendering
- Change condition from 'readingProgress && readingProgress > 0' to 'readingProgress !== undefined && readingProgress > 0'
- Prevent React from rendering 0 values when readingProgress is 0
- Fix weird 0 artefacts appearing in compact list view
- Ensure clean conditional rendering without unwanted text output
2025-10-25 01:53:52 +02:00
Gigi
e3cd4454b4 fix: only show reading progress bar in compact view when there is actual progress
- Add conditional rendering for ReadingProgressBar in CompactView
- Only display progress bar when readingProgress > 0
- Remove empty progress bar separator from compact list view
- Maintain clean, minimal compact view without unnecessary visual elements
- Keep progress bar functionality for cards with actual reading progress
2025-10-25 01:52:57 +02:00
Gigi
78bc1f46dd style: eliminate excessive space between progress bar and footer
- Reduce ReadingProgressBar margins from 0.25rem to 0.125rem (75% reduction)
- Reduce bookmark footer padding-top from 0.25rem to 0.125rem (75% reduction)
- Reduce reading progress separator margin from 0.25rem to 0.125rem (75% reduction)
- Update responsive breakpoints for ultra-compact spacing
- Achieve minimal gap between progress bar and date/author footer
- Create ultra-tight vertical layout with almost no wasted space
2025-10-25 01:52:35 +02:00
Gigi
c8cd1e6e66 style: make bookmark cards significantly more compact
- Reduce card content gap from 0.5rem to 0.25rem (50% reduction)
- Reduce bookmark title margin-bottom from 0.5rem to 0.25rem (50% reduction)
- Reduce bookmark footer padding-top from 0.5rem to 0.25rem (50% reduction)
- Reduce reading progress separator margin from 0.5rem to 0.25rem (50% reduction)
- Update ReadingProgressBar component margins to 0.25rem
- Update responsive breakpoints for consistent compact spacing
- Achieve much tighter vertical layout with minimal wasted space
2025-10-25 01:50:46 +02:00
Gigi
5254697fe2 refactor: create reusable ReadingProgressBar component for DRY code
- Create ReadingProgressBar component with configurable props
- Remove duplicated progress color logic from all view components
- Replace inline progress bar code with reusable component
- Maintain consistent behavior across CardView, CompactView, and LargeView
- Reduce code duplication and improve maintainability
2025-10-25 01:49:45 +02:00
Gigi
13462efaed feat: ensure reading progress bar shows for all bookmark types across all view modes
- Update CompactView to always show progress bar for all bookmark types
- Update LargeView to always show progress bar for all bookmark types
- Remove conditional logic that only showed progress for articles
- Ensure consistent visual separator across CardView, CompactView, and LargeView
- Maintain empty state display (1px border line) when no progress available
2025-10-25 01:48:42 +02:00
Gigi
1df00fbfda feat: show reading progress bar for all bookmark types
- Remove isArticle condition from reading progress bar display
- Show progress bar for videos, links, and articles
- Maintain consistent visual separator for all bookmark types
- Ensure reading progress tracking works across all content types
2025-10-25 01:48:08 +02:00
Gigi
c2e220a1f2 style: reduce vertical spacing in medium-sized bookmark cards
- Reduce reading progress separator margin from 0.75rem to 0.5rem
- Reduce card content gap from 0.75rem to 0.5rem
- Reduce bookmark title margin-bottom from 0.75rem to 0.5rem
- Reduce bookmark footer padding-top from 0.75rem to 0.5rem
- Update responsive breakpoints for consistent compact spacing
- Make cards more compact and visually tighter
2025-10-25 01:47:46 +02:00
Gigi
00740aab6d fix: ensure empty reading progress bar is always visible for articles
- Change progress bar background from transparent to border color when no progress
- Reading progress separator now always shows 1px line for articles
- Maintains visual consistency between articles with and without reading progress
- Ensures proper visual separation between content and footer
2025-10-25 01:45:53 +02:00
Gigi
e12d67cc5f feat: remove type icon from medium-sized bookmark cards
- Remove contentTypeIcon from CardViewProps interface
- Remove type icon display from bookmark footer
- Replace contentTypeIcon with faLink in thumbnail placeholder
- Simplify card interface by removing content type indicator
- Clean up unused icon-related code
2025-10-25 01:45:29 +02:00
Gigi
e12aaa2b6c feat: remove text expansion mechanic from medium-sized cards
- Remove expanded state and shouldTruncate logic
- Remove chevron icons and expand/collapse buttons
- Simplify content display to show full content without truncation
- Remove unused faChevronDown and faChevronUp imports
- Streamline card interface for cleaner, simpler design
2025-10-25 01:44:16 +02:00
Gigi
9880a9ae34 fix: ensure timestamp and icon display on same line
- Change bookmark-type display to inline-flex for inline layout
- Add flex-wrap: nowrap to prevent wrapping
- Set timestamp elements to display: inline with white-space: nowrap
- Reduce gap between timestamp and icon to 0.5rem for tighter spacing
- Ensure both elements stay on the same horizontal line
2025-10-25 01:43:22 +02:00
Gigi
603db680f2 style: match type icon color to author text color
- Set type icon color to var(--color-text-secondary) to match author text
- Icon now has same muted color as author link instead of bright white
- Creates better visual consistency in footer metadata
2025-10-25 01:42:02 +02:00
Gigi
ae0471946e feat: move timestamp to footer next to type icon
- Move timestamp from header to footer positioned next to type icon
- Create bookmark-footer-right container for timestamp and type icon
- Hide empty bookmark-header since timestamp is now in footer
- Update footer layout: author (left), timestamp + type icon (right)
- Maintain proper spacing and alignment for all elements
2025-10-25 01:41:40 +02:00
Gigi
a48308d57d fix: remove primary color from global bookmark-type rule
- Remove color: var(--color-primary) from global .bookmark-type rule
- Icon was still blue due to global CSS rule overriding footer-specific rule
- Now uses default text color for subtle appearance
2025-10-25 01:41:05 +02:00
Gigi
f67b358148 style: remove primary color from bookmark type icon
- Remove var(--color-primary) color from bookmark type icon
- Use default text color for more subtle icon appearance
- Maintain font size and spacing for consistent layout
2025-10-25 01:40:08 +02:00
Gigi
46a0a3da1f feat: add title display for regular bookmarks/links
- Extract title from tags for all bookmark types, not just articles
- Display titles for regular bookmarks that have title tags
- Support both article titles and bookmark titles in card display
- Maintain existing article title functionality
- Improve title coverage across all bookmark types
2025-10-25 01:39:46 +02:00
Gigi
c92a620ea8 feat: move bookmark type icon to bottom right footer
- Remove type icon from header and move to footer
- Position author name on left, type icon on right in footer
- Update header to right-align date only
- Add flex layout to footer for proper spacing
- Maintain consistent styling and responsive design
2025-10-25 01:39:08 +02:00
Gigi
34de372509 feat: remove URL display from medium-sized bookmark cards
- Remove bookmark URLs section from CardView component
- Remove unused urlsExpanded state variable
- Clean up unused URL styling from CSS
- Simplify card layout by removing URL display
- Maintain all other card functionality (title, content, progress, author)
2025-10-25 01:38:38 +02:00
Gigi
a422084949 feat: add title display to medium-sized bookmark cards
- Add articleTitle prop to CardView component interface
- Display article titles for kind:30023 articles in card layout
- Style titles with proper typography and responsive design
- Position titles between header and URLs for optimal hierarchy
- Add line clamping for long titles (2 lines max)
- Update BookmarkItem to pass articleTitle to CardView
2025-10-25 01:38:11 +02:00
Gigi
bd0e075984 fix: remove unused variables to resolve linting errors
- Remove unused imageLoading and imageError state variables
- Clean up CardView component to pass ESLint checks
- Maintain all existing functionality while fixing linting issues
2025-10-25 01:37:04 +02:00
Gigi
38f4b69d48 feat: position bookmark type icon in top-left corner of card
- Move bookmark type icon to top-left corner as overlay
- Add bookmark-type-overlay with absolute positioning
- Style icon with background, border, and shadow for visibility
- Update responsive design for smaller screens
- Remove icon from bookmark header to avoid duplication
- Ensure icon is always visible and accessible
2025-10-25 01:36:01 +02:00
Gigi
9d1d944daf feat: position reading progress bar to span full card width
- Move reading progress bar outside of text content area
- Position progress bar between content and author name
- Update CSS to remove card-content scoping for full-width display
- Maintain 1px thickness and smooth transitions
- Ensure progress bar spans entire card width for better visual separation
2025-10-25 01:35:15 +02:00
Gigi
e56461cb12 feat: restructure card layout to position author in bottom-left corner
- Move thumbnail to be next to text content instead of blocking author position
- Create card-content-header with thumbnail + text-content flex layout
- Position author name in bottom-left corner of card footer
- Update responsive design for new layout structure
- Maintain thumbnail functionality while fixing author positioning
2025-10-25 01:34:40 +02:00
Gigi
f6b6747f09 feat: always show reading progress bar as 1px separator
- Show reading progress bar for all article cards, even without progress
- Change progress bar thickness from 4px to 1px for subtle separation
- Remove fallback separator since progress bar is always shown
- Empty progress bars show as transparent fill with border background
- Maintain consistent visual separation across all article cards
2025-10-25 01:33:14 +02:00
Gigi
180c26c47a feat: use reading progress bar as visual separator
- Remove separate border separator from bookmark footer
- Enhance reading progress bar styling as primary separator
- Add subtle separator for cards without reading progress
- Improve visual hierarchy with progress-based separation
- Maintain consistent spacing and visual flow
2025-10-25 01:32:34 +02:00
Gigi
78da0cb3e4 feat: redesign medium-sized bookmark cards with left-side thumbnails
- Replace full-width hero images with compact left-side thumbnails
- Add card-layout flex container for thumbnail + content arrangement
- Implement 80px square thumbnails with hover scale effects
- Update responsive design for smaller screens (60px tablet, 50px mobile)
- Maintain content truncation and reading progress indicators
- Improve space efficiency while preserving visual appeal
2025-10-25 01:31:26 +02:00
Gigi
3d74c25c7d feat: enhance medium-sized bookmark cards with improved styling and layout
- Add card-view class for better visual hierarchy
- Implement hero image display with fallback placeholder
- Add responsive design for mobile and tablet screens
- Improve content truncation with line clamping
- Enhance URL display with better styling
- Add hover effects and smooth transitions
- Optimize card layout for better readability
2025-10-25 01:30:23 +02:00
Gigi
f46f55705b docs: update CHANGELOG for v0.10.24 2025-10-25 01:28:44 +02:00
8 changed files with 452 additions and 183 deletions

View File

@@ -7,6 +7,32 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
## [0.10.24] - 2025-01-27
### Added
- Dynamic browser title based on content
- Browser title now updates to reflect the current content being viewed
- Improves user experience and bookmark identification
- Enhanced Links type bookmarks with OpenGraph data
- Web bookmarks now include rich OpenGraph metadata
- Better visual representation and content preview for web bookmarks
### Changed
- Replaced custom OpenGraph extraction with fetch-opengraph library
- More reliable and standardized OpenGraph data extraction
- Better performance and maintainability for web bookmark processing
### Fixed
- Description extraction from web bookmark content field
- Improved extraction of descriptions from web bookmark content
- Better content identification and display for web bookmarks
- Resolved all linting and TypeScript issues
- Code quality improvements and type safety enhancements
- Cleaner codebase with better maintainability
## [0.10.23] - 2025-01-27
### Added

View File

@@ -1,6 +1,6 @@
{
"name": "boris",
"version": "0.10.24",
"version": "0.10.25",
"description": "A minimal nostr client for bookmark management",
"homepage": "https://read.withboris.com/",
"type": "module",

View File

@@ -169,5 +169,5 @@ export const BookmarkItem: React.FC<BookmarkItemProps> = ({ bookmark, index, onS
return <LargeView {...sharedProps} getIconForUrlType={getIconForUrlType} previewImage={previewImage} />
}
return <CardView {...sharedProps} articleImage={articleImage} />
return <CardView {...sharedProps} articleImage={articleImage} articleTitle={articleTitle} />
}

View File

@@ -1,8 +1,9 @@
import React, { useState } from 'react'
import { Link } from 'react-router-dom'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faChevronDown, faChevronUp } from '@fortawesome/free-solid-svg-icons'
import { IconDefinition } from '@fortawesome/fontawesome-svg-core'
import { faLink } from '@fortawesome/free-solid-svg-icons'
import { faNewspaper, faStickyNote, faCirclePlay, faCamera, faFileLines } from '@fortawesome/free-regular-svg-icons'
import { faGlobe } from '@fortawesome/free-solid-svg-icons'
import { IndividualBookmark } from '../../types/bookmarks'
import { formatDate, renderParsedContent } from '../../utils/bookmarkUtils'
import RichContent from '../RichContent'
@@ -10,6 +11,7 @@ import { classifyUrl } from '../../utils/helpers'
import { useImageCache } from '../../hooks/useImageCache'
import { getPreviewImage, fetchOgImage } from '../../utils/imagePreview'
import { naddrEncode } from 'nostr-tools/nip19'
import { ReadingProgressBar } from '../ReadingProgressBar'
interface CardViewProps {
bookmark: IndividualBookmark
@@ -22,7 +24,7 @@ interface CardViewProps {
handleReadNow: (e: React.MouseEvent<HTMLButtonElement>) => void
articleImage?: string
articleSummary?: string
contentTypeIcon: IconDefinition
articleTitle?: string
readingProgress?: number
}
@@ -31,13 +33,12 @@ export const CardView: React.FC<CardViewProps> = ({
index,
hasUrls,
extractedUrls,
onSelectUrl,
authorNpub,
getAuthorDisplayName,
handleReadNow,
articleImage,
articleSummary,
contentTypeIcon,
articleTitle,
readingProgress
}) => {
const firstUrl = hasUrls ? extractedUrls[0] : null
@@ -45,21 +46,41 @@ export const CardView: React.FC<CardViewProps> = ({
const instantPreview = firstUrl ? getPreviewImage(firstUrl, firstUrlClassificationType || '') : null
const [ogImage, setOgImage] = useState<string | null>(null)
const [expanded, setExpanded] = useState(false)
const [urlsExpanded, setUrlsExpanded] = useState(false)
const contentLength = (bookmark.content || '').length
const shouldTruncate = !expanded && contentLength > 210
const isArticle = bookmark.kind === 30023
const isWebBookmark = bookmark.kind === 39701
const isNote = bookmark.kind === 1
// Calculate progress color (matching BlogPostCard logic)
let progressColor = '#6366f1' // Default blue (reading)
if (readingProgress && readingProgress >= 0.95) {
progressColor = '#10b981' // Green (completed)
} else if (readingProgress && readingProgress > 0 && readingProgress <= 0.10) {
progressColor = 'var(--color-text)' // Neutral text color (started)
// Extract title from tags for regular bookmarks (not just articles)
const bookmarkTitle = bookmark.tags.find(t => t[0] === 'title')?.[1]
// Get content type icon based on bookmark kind and URL classification
const getContentTypeIcon = () => {
if (isArticle) return faNewspaper // Nostr-native article
// For web bookmarks, classify the URL to determine icon
if (isWebBookmark && firstUrlClassificationType) {
switch (firstUrlClassificationType) {
case 'youtube':
case 'video':
return faCirclePlay
case 'image':
return faCamera
case 'article':
return faFileLines
default:
return faGlobe
}
}
// For notes, use sticky note icon
if (isNote) return faStickyNote
// Default fallback
return faLink
}
// Determine which image to use (article image, instant preview, or OG image)
const previewImage = articleImage || instantPreview || ogImage
const cachedImage = useImageCache(previewImage || undefined)
@@ -71,6 +92,7 @@ export const CardView: React.FC<CardViewProps> = ({
}
}, [firstUrl, articleImage, instantPreview, ogImage])
const triggerOpen = () => handleReadNow({ preventDefault: () => {} } as React.MouseEvent<HTMLButtonElement>)
const handleKeyDown: React.KeyboardEventHandler<HTMLDivElement> = (e) => {
@@ -106,122 +128,87 @@ export const CardView: React.FC<CardViewProps> = ({
return (
<div
key={`${bookmark.id}-${index}`}
className={`individual-bookmark ${bookmark.isPrivate ? 'private-bookmark' : ''}`}
className={`individual-bookmark card-view ${bookmark.isPrivate ? 'private-bookmark' : ''}`}
onClick={triggerOpen}
role="button"
tabIndex={0}
onKeyDown={handleKeyDown}
>
{cachedImage && (
<div
className="article-hero-image"
style={{ backgroundImage: `url(${cachedImage})` }}
onClick={() => handleReadNow({ preventDefault: () => {} } as React.MouseEvent<HTMLButtonElement>)}
/>
)}
<div className="bookmark-header">
<span className="bookmark-type">
<FontAwesomeIcon icon={contentTypeIcon} className="content-type-icon" />
</span>
{getInternalRoute() ? (
<Link
to={getInternalRoute()!}
className="bookmark-date-link"
title="Open in app"
onClick={(e) => e.stopPropagation()}
>
{formatDate(bookmark.created_at ?? bookmark.listUpdatedAt)}
</Link>
) : (
<span className="bookmark-date">{formatDate(bookmark.created_at ?? bookmark.listUpdatedAt)}</span>
)}
</div>
{extractedUrls.length > 0 && (
<div className="bookmark-urls">
{(urlsExpanded ? extractedUrls : extractedUrls.slice(0, 1)).map((url, urlIndex) => {
return (
<button
key={urlIndex}
className="bookmark-url"
onClick={(e) => { e.stopPropagation(); onSelectUrl?.(url) }}
title="Open in reader"
<div className="card-layout">
<div className="card-content">
<div className="card-content-header">
{(cachedImage || firstUrl) && (
<div
className="card-thumbnail"
style={cachedImage ? { backgroundImage: `url(${cachedImage})` } : undefined}
onClick={() => handleReadNow({ preventDefault: () => {} } as React.MouseEvent<HTMLButtonElement>)}
>
{url}
</button>
)
})}
{extractedUrls.length > 1 && (
<button
className="expand-toggle-urls"
onClick={(e) => { e.stopPropagation(); setUrlsExpanded(v => !v) }}
aria-label={urlsExpanded ? 'Collapse URLs' : 'Expand URLs'}
title={urlsExpanded ? 'Collapse URLs' : 'Expand URLs'}
{!cachedImage && firstUrl && (
<div className="thumbnail-placeholder">
<FontAwesomeIcon icon={getContentTypeIcon()} />
</div>
)}
</div>
)}
<div className="card-text-content">
<div className="bookmark-header">
</div>
{/* Display title for articles or bookmarks with titles */}
{(articleTitle || bookmarkTitle) && (
<h3 className="bookmark-title">
<RichContent content={articleTitle || bookmarkTitle || ''} className="" />
</h3>
)}
{isArticle && articleSummary ? (
<RichContent content={articleSummary} className="bookmark-content article-summary" />
) : bookmark.parsedContent ? (
<div className="bookmark-content">
{renderParsedContent(bookmark.parsedContent)}
</div>
) : bookmark.content && (
<RichContent content={bookmark.content} className="bookmark-content" />
)}
</div>
</div>
</div>
{/* Reading progress indicator as separator - always shown for all bookmark types */}
<ReadingProgressBar
readingProgress={readingProgress}
height={1}
marginTop="0.125rem"
marginBottom="0.125rem"
/>
<div className="bookmark-footer">
<div className="bookmark-meta-minimal">
<Link
to={`/p/${authorNpub}`}
className="author-link-minimal"
title="Open author profile"
onClick={(e) => e.stopPropagation()}
>
{urlsExpanded ? `Hide ${extractedUrls.length - 1} more` : `Show ${extractedUrls.length - 1} more`}
</button>
)}
{getAuthorDisplayName()}
</Link>
</div>
<div className="bookmark-footer-right">
{getInternalRoute() ? (
<Link
to={getInternalRoute()!}
className="bookmark-date-link"
title="Open in app"
onClick={(e) => e.stopPropagation()}
>
{formatDate(bookmark.created_at ?? bookmark.listUpdatedAt)}
</Link>
) : (
<span className="bookmark-date">{formatDate(bookmark.created_at ?? bookmark.listUpdatedAt)}</span>
)}
</div>
</div>
)}
{isArticle && articleSummary ? (
<RichContent content={articleSummary} className="bookmark-content article-summary" />
) : bookmark.parsedContent ? (
<div className="bookmark-content">
{shouldTruncate && bookmark.content
? <RichContent content={`${bookmark.content.slice(0, 210).trimEnd()}`} className="" />
: renderParsedContent(bookmark.parsedContent)}
</div>
) : bookmark.content && (
<RichContent content={shouldTruncate ? `${bookmark.content.slice(0, 210).trimEnd()}` : bookmark.content} />
)}
{contentLength > 210 && (
<button
className="expand-toggle"
onClick={(e) => { e.stopPropagation(); setExpanded(v => !v) }}
aria-label={expanded ? 'Collapse' : 'Expand'}
title={expanded ? 'Collapse' : 'Expand'}
>
<FontAwesomeIcon icon={expanded ? faChevronUp : faChevronDown} />
</button>
)}
{/* Reading progress indicator for articles */}
{isArticle && readingProgress !== undefined && readingProgress > 0 && (
<div
style={{
height: '3px',
width: '100%',
background: 'var(--color-border)',
overflow: 'hidden',
marginTop: '0.75rem'
}}
>
<div
style={{
height: '100%',
width: `${Math.round(readingProgress * 100)}%`,
background: progressColor,
transition: 'width 0.3s ease, background 0.3s ease'
}}
/>
</div>
)}
<div className="bookmark-footer">
<div className="bookmark-meta-minimal">
<Link
to={`/p/${authorNpub}`}
className="author-link-minimal"
title="Open author profile"
onClick={(e) => e.stopPropagation()}
>
{getAuthorDisplayName()}
</Link>
</div>
{/* CTA removed */}
</div>
</div>
)

View File

@@ -6,6 +6,7 @@ import { IndividualBookmark } from '../../types/bookmarks'
import { formatDateCompact } from '../../utils/bookmarkUtils'
import RichContent from '../RichContent'
import { naddrEncode } from 'nostr-tools/nip19'
import { ReadingProgressBar } from '../ReadingProgressBar'
interface CompactViewProps {
bookmark: IndividualBookmark
@@ -36,13 +37,6 @@ export const CompactView: React.FC<CompactViewProps> = ({
const displayText = isArticle && articleTitle ? articleTitle : bookmark.content
// Calculate progress color
let progressColor = '#6366f1' // Default blue (reading)
if (readingProgress && readingProgress >= 0.95) {
progressColor = '#10b981' // Green (completed)
} else if (readingProgress && readingProgress > 0 && readingProgress <= 0.10) {
progressColor = 'var(--color-text)' // Neutral text color (started)
}
const handleCompactClick = () => {
if (isArticle) {
@@ -86,27 +80,13 @@ export const CompactView: React.FC<CompactViewProps> = ({
{/* CTA removed */}
</div>
{/* Reading progress indicator for all bookmark types with reading data */}
{/* Reading progress indicator - only show when there's actual progress */}
{readingProgress !== undefined && readingProgress > 0 && (
<div
style={{
height: '1px',
width: '100%',
background: 'var(--color-border)',
overflow: 'hidden',
margin: '0',
marginLeft: '1.5rem'
}}
>
<div
style={{
height: '100%',
width: `${Math.round(readingProgress * 100)}%`,
background: progressColor,
transition: 'width 0.3s ease, background 0.3s ease'
}}
/>
</div>
<ReadingProgressBar
readingProgress={readingProgress}
height={1}
marginLeft="1.5rem"
/>
)}
</div>
)

View File

@@ -8,6 +8,7 @@ import RichContent from '../RichContent'
import { IconGetter } from './shared'
import { useImageCache } from '../../hooks/useImageCache'
import { naddrEncode } from 'nostr-tools/nip19'
import { ReadingProgressBar } from '../ReadingProgressBar'
interface LargeViewProps {
bookmark: IndividualBookmark
@@ -43,15 +44,6 @@ export const LargeView: React.FC<LargeViewProps> = ({
const cachedImage = useImageCache(previewImage || undefined)
const isArticle = bookmark.kind === 30023
// Calculate progress display (matching readingProgressUtils.ts logic)
const progressPercent = readingProgress ? Math.round(readingProgress * 100) : 0
let progressColor = '#6366f1' // Default blue (reading)
if (readingProgress && readingProgress >= 0.95) {
progressColor = '#10b981' // Green (completed)
} else if (readingProgress && readingProgress > 0 && readingProgress <= 0.10) {
progressColor = 'var(--color-text)' // Neutral text color (started)
}
const triggerOpen = () => handleReadNow({ preventDefault: () => {} } as React.MouseEvent<HTMLButtonElement>)
const handleKeyDown: React.KeyboardEventHandler<HTMLDivElement> = (e) => {
@@ -122,27 +114,12 @@ export const LargeView: React.FC<LargeViewProps> = ({
<RichContent content={bookmark.content} className="large-text" />
)}
{/* Reading progress indicator for articles - shown only if there's progress */}
{isArticle && readingProgress !== undefined && readingProgress > 0 && (
<div
style={{
height: '3px',
width: '100%',
background: 'var(--color-border)',
overflow: 'hidden',
marginTop: '0.75rem'
}}
>
<div
style={{
height: '100%',
width: `${progressPercent}%`,
background: progressColor,
transition: 'width 0.3s ease, background 0.3s ease'
}}
/>
</div>
)}
{/* Reading progress indicator for all bookmark types - always shown */}
<ReadingProgressBar
readingProgress={readingProgress}
height={3}
marginTop="0.75rem"
/>
<div className="large-footer">
<span className="bookmark-type-large">

View File

@@ -0,0 +1,58 @@
import React from 'react'
interface ReadingProgressBarProps {
readingProgress?: number
height?: number
marginTop?: string
marginBottom?: string
marginLeft?: string
className?: string
}
export const ReadingProgressBar: React.FC<ReadingProgressBarProps> = ({
readingProgress,
height = 1,
marginTop,
marginBottom,
marginLeft,
className
}) => {
// Calculate progress color
let progressColor = '#6366f1' // Default blue (reading)
if (readingProgress && readingProgress >= 0.95) {
progressColor = '#10b981' // Green (completed)
} else if (readingProgress && readingProgress > 0 && readingProgress <= 0.10) {
progressColor = 'var(--color-text)' // Neutral text color (started)
}
const progressWidth = readingProgress ? `${Math.round(readingProgress * 100)}%` : '0%'
const progressBackground = readingProgress ? progressColor : 'var(--color-border)'
return (
<div
className={className}
style={{
height: `${height}px`,
width: '100%',
background: 'var(--color-border)',
borderRadius: '0.5px',
overflow: 'hidden',
marginTop,
marginBottom,
marginLeft,
position: 'relative',
minHeight: `${height}px`
}}
>
<div
style={{
height: '100%',
width: progressWidth,
background: progressBackground,
transition: 'width 0.3s ease, background 0.3s ease',
minHeight: `${height}px`
}}
/>
</div>
)
}

View File

@@ -59,7 +59,7 @@
.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: var(--color-primary); font-size: 0.9rem; display: flex; align-items: center; gap: 0.35rem; }
.bookmark-type { 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; }
@@ -76,6 +76,179 @@
.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); }
/* Medium-sized card view */
.individual-bookmark.card-view {
padding: 0;
display: flex;
flex-direction: column;
overflow: hidden;
border: 1px solid var(--color-bg-elevated);
border-radius: 12px;
transition: all 0.3s ease;
background: var(--color-bg);
}
.individual-bookmark.card-view:hover {
border-color: var(--color-border);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
transform: translateY(-2px);
}
.card-layout {
display: flex;
flex-direction: column;
padding: 1.25rem;
gap: 0.75rem;
}
.card-content-header {
display: flex;
gap: 1rem;
align-items: flex-start;
}
.card-text-content {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.card-thumbnail {
width: 80px;
height: 80px;
flex-shrink: 0;
background: linear-gradient(135deg, var(--color-bg-elevated) 0%, var(--color-bg-subtle) 50%, var(--color-bg-elevated) 100%);
background-size: cover;
background-position: center;
background-repeat: no-repeat;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.3s ease;
position: relative;
overflow: hidden;
}
.card-thumbnail:hover {
opacity: 0.9;
transform: scale(1.05);
}
.card-thumbnail::after {
content: '';
position: absolute;
inset: 0;
background: linear-gradient(to bottom, transparent 60%, rgba(0,0,0,0.1) 100%);
pointer-events: none;
}
.card-thumbnail .thumbnail-placeholder {
font-size: 1.5rem;
color: var(--color-border-subtle);
opacity: 0.6;
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
}
.card-content {
display: flex;
flex-direction: column;
gap: 0.25rem;
flex: 1;
}
.card-content .bookmark-header {
display: none;
}
.card-content .bookmark-title {
font-size: 1.1rem;
font-weight: 600;
color: var(--color-text);
margin: 0 0 0.25rem 0;
line-height: 1.4;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
.card-content .bookmark-content {
font-size: 0.9rem;
line-height: 1.6;
color: var(--color-text);
margin: 0;
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
}
.card-content .bookmark-content.article-summary {
-webkit-line-clamp: 2;
font-style: italic;
color: var(--color-text-secondary);
}
.card-content .bookmark-footer {
margin-top: auto;
padding-top: 0.125rem;
display: flex;
justify-content: space-between;
align-items: center;
}
.card-content .bookmark-footer-right {
display: flex;
align-items: center;
gap: 0.5rem;
flex-wrap: nowrap;
}
.card-content .bookmark-footer .bookmark-type {
font-size: 0.9rem;
display: inline-flex;
align-items: center;
gap: 0.35rem;
color: var(--color-text-secondary) !important;
}
.card-content .bookmark-footer .bookmark-type .content-type-icon {
color: var(--color-text-secondary) !important;
}
.card-content .bookmark-footer .bookmark-date,
.card-content .bookmark-footer .bookmark-date-link {
display: inline;
white-space: nowrap;
}
/* Reading progress as separator - always shown, full width */
.reading-progress-separator {
height: 1px;
width: 100%;
background: var(--color-border);
border-radius: 0.5px;
overflow: hidden;
margin: 0.125rem 0;
position: relative;
}
.reading-progress-separator .progress-fill {
height: 100%;
border-radius: 0.5px;
transition: width 0.3s ease, background 0.3s ease;
}
/* Large preview view */
.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: linear-gradient(135deg, var(--color-bg-elevated) 0%, var(--color-bg-subtle) 50%, var(--color-bg-elevated) 100%); 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; }
@@ -115,6 +288,74 @@
.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; }
/* Responsive design for medium-sized cards */
@media (max-width: 768px) {
.individual-bookmark.card-view {
border-radius: 8px;
}
.card-layout {
padding: 1rem;
gap: 0.5rem;
}
.card-content-header {
gap: 0.75rem;
}
.card-thumbnail {
width: 60px;
height: 60px;
}
.card-text-content {
gap: 0.5rem;
}
.card-content .bookmark-title {
font-size: 1rem;
margin-bottom: 0.25rem;
}
.card-content .bookmark-content {
font-size: 0.85rem;
line-height: 1.5;
}
.card-content .bookmark-footer {
padding-top: 0.125rem;
}
}
@media (max-width: 480px) {
.card-layout {
padding: 0.75rem;
gap: 0.5rem;
}
.card-content-header {
gap: 0.5rem;
}
.card-thumbnail {
width: 50px;
height: 50px;
}
.card-thumbnail .thumbnail-placeholder {
font-size: 1.2rem;
}
.card-content .bookmark-title {
font-size: 0.9rem;
margin-bottom: 0.25rem;
}
.card-content .bookmark-content {
font-size: 0.8rem;
}
}
@media (max-width: 768px) {
.explore-container { padding: 1rem; }
.explore-header h1 { font-size: 2rem; }