From 96ce12b952188e5ad86683e4c8a9312c721a9e28 Mon Sep 17 00:00:00 2001 From: Gigi Date: Mon, 13 Oct 2025 21:01:44 +0200 Subject: [PATCH 01/19] feat: add reading position tracking with visual progress indicator - Install position-indicator library for scroll position tracking - Create useReadingPosition hook for position management - Add ReadingProgressIndicator component with animated progress bar - Integrate reading progress in ContentPanel for text content only - Add CSS styles for fixed progress indicator with shimmer animation - Track reading completion at 90% threshold - Exclude video content from position tracking --- package-lock.json | 11 ++- package.json | 1 + src/components/ContentPanel.tsx | 23 ++++++ src/components/ReadingProgressIndicator.tsx | 31 ++++++++ src/hooks/useReadingPosition.ts | 83 ++++++++++++++++++++ src/styles/components/reader.css | 85 +++++++++++++++++++++ 6 files changed, 232 insertions(+), 2 deletions(-) create mode 100644 src/components/ReadingProgressIndicator.tsx create mode 100644 src/hooks/useReadingPosition.ts diff --git a/package-lock.json b/package-lock.json index dae46769..3e2a4151 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "boris", - "version": "0.5.6", + "version": "0.5.7", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "boris", - "version": "0.5.6", + "version": "0.5.7", "dependencies": { "@fortawesome/fontawesome-svg-core": "^7.1.0", "@fortawesome/free-solid-svg-icons": "^7.1.0", @@ -22,6 +22,7 @@ "applesauce-relay": "^4.0.0", "date-fns": "^4.1.0", "nostr-tools": "^2.4.0", + "position-indicator": "^0.0.12", "prismjs": "^1.30.0", "react": "^18.2.0", "react-dom": "^18.2.0", @@ -8972,6 +8973,12 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/position-indicator": { + "version": "0.0.12", + "resolved": "https://registry.npmjs.org/position-indicator/-/position-indicator-0.0.12.tgz", + "integrity": "sha512-qHQejEylblB7rZ3MfXSI5hu1+Dq7EBv1BYwUIVWzJ3nZ8d6V7LFBi1zC5/XwT/01Wxddf9kaFoOy3L70/5tC+A==", + "license": "MIT" + }, "node_modules/possible-typed-array-names": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", diff --git a/package.json b/package.json index 1d2be76d..5b459a8c 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "applesauce-relay": "^4.0.0", "date-fns": "^4.1.0", "nostr-tools": "^2.4.0", + "position-indicator": "^0.0.12", "prismjs": "^1.30.0", "react": "^18.2.0", "react-dom": "^18.2.0", diff --git a/src/components/ContentPanel.tsx b/src/components/ContentPanel.tsx index 581a46dc..89b76023 100644 --- a/src/components/ContentPanel.tsx +++ b/src/components/ContentPanel.tsx @@ -33,6 +33,8 @@ import { faBooks } from '../icons/customIcons' import { extractYouTubeId, getYouTubeMeta } from '../services/youtubeMetaService' import { classifyUrl } from '../utils/helpers' import { buildNativeVideoUrl } from '../utils/videoHelpers' +import { useReadingPosition } from '../hooks/useReadingPosition' +import { ReadingProgressIndicator } from './ReadingProgressIndicator' interface ContentPanelProps { loading: boolean @@ -115,6 +117,18 @@ const ContentPanel: React.FC = ({ onClearSelection }) + // Reading position tracking - only for text content, not videos + const isTextContent = !loading && !!(markdown || html) && !selectedUrl?.includes('youtube') && !selectedUrl?.includes('vimeo') + const { isReadingComplete, progressPercentage } = useReadingPosition({ + enabled: isTextContent, + onReadingComplete: () => { + // Optional: Auto-mark as read when reading is complete + if (activeAccount && !isMarkedAsRead) { + // Could trigger auto-mark as read here if desired + } + } + }) + // Close menu when clicking outside useEffect(() => { const handleClickOutside = (event: MouseEvent) => { @@ -361,6 +375,15 @@ const ContentPanel: React.FC = ({ return (
+ {/* Reading Progress Indicator */} + {isTextContent && ( + + )} + {/* Hidden markdown preview to convert markdown to HTML */} {markdown && (
diff --git a/src/components/ReadingProgressIndicator.tsx b/src/components/ReadingProgressIndicator.tsx new file mode 100644 index 00000000..bc7ef875 --- /dev/null +++ b/src/components/ReadingProgressIndicator.tsx @@ -0,0 +1,31 @@ +import React from 'react' + +interface ReadingProgressIndicatorProps { + progress: number // 0 to 100 + isComplete?: boolean + showPercentage?: boolean + className?: string +} + +export const ReadingProgressIndicator: React.FC = ({ + progress, + isComplete = false, + showPercentage = true, + className = '' +}) => { + return ( +
+
+
+
+ {showPercentage && ( +
+ {isComplete ? '✓ Complete' : `${progress}%`} +
+ )} +
+ ) +} diff --git a/src/hooks/useReadingPosition.ts b/src/hooks/useReadingPosition.ts new file mode 100644 index 00000000..594aa9a0 --- /dev/null +++ b/src/hooks/useReadingPosition.ts @@ -0,0 +1,83 @@ +import { useEffect, useRef, useState } from 'react' +// @ts-ignore - position-indicator types issue +import { createPositionIndicator } from 'position-indicator' + +interface ReadingPositionData { + position: number // 0 to 1 + prevPosition: number + hasUpdated: boolean + hasScroll: boolean + eventType: 'scroll' | 'resize' | 'heightChange' | 'init' + eventDate: number +} + +interface UseReadingPositionOptions { + enabled?: boolean + onPositionChange?: (data: ReadingPositionData) => void + onReadingComplete?: () => void + readingCompleteThreshold?: number // Default 0.9 (90%) +} + +export const useReadingPosition = ({ + enabled = true, + onPositionChange, + onReadingComplete, + readingCompleteThreshold = 0.9 +}: UseReadingPositionOptions = {}) => { + const [position, setPosition] = useState(0) + const [isReadingComplete, setIsReadingComplete] = useState(false) + const positionIndicatorRef = useRef(null) + const hasTriggeredComplete = useRef(false) + + useEffect(() => { + if (!enabled) return + + const handleInit = (data: ReadingPositionData) => { + setPosition(data.position) + onPositionChange?.(data) + } + + const handleUpdate = (data: ReadingPositionData) => { + setPosition(data.position) + onPositionChange?.(data) + + // Check if reading is complete + if (data.position >= readingCompleteThreshold && !hasTriggeredComplete.current) { + setIsReadingComplete(true) + hasTriggeredComplete.current = true + onReadingComplete?.() + } + } + + const positionIndicator = createPositionIndicator({ + onInit: handleInit, + onUpdate: handleUpdate, + useResizeListener: true, + useResizeObserver: true + }) + + positionIndicator.init() + positionIndicatorRef.current = positionIndicator + + return () => { + if (positionIndicatorRef.current) { + positionIndicatorRef.current.destroy() + positionIndicatorRef.current = null + } + } + }, [enabled, onPositionChange, onReadingComplete, readingCompleteThreshold]) + + // Reset reading complete state when enabled changes + useEffect(() => { + if (!enabled) { + setIsReadingComplete(false) + hasTriggeredComplete.current = false + } + }, [enabled]) + + return { + position, + isReadingComplete, + progressPercentage: Math.round(position * 100) + } +} diff --git a/src/styles/components/reader.css b/src/styles/components/reader.css index 729e5b70..69c726e4 100644 --- a/src/styles/components/reader.css +++ b/src/styles/components/reader.css @@ -102,4 +102,89 @@ .reader-header-overlay .reader-title { font-size: 1.5rem; line-height: 1.3; } } +/* Reading Progress Indicator */ +.reading-progress-indicator { + position: fixed; + top: 0; + left: 0; + right: 0; + z-index: 1000; + background: rgba(26, 26, 26, 0.9); + backdrop-filter: blur(8px); + border-bottom: 1px solid rgba(255, 255, 255, 0.1); + padding: 0.5rem 1rem; + display: flex; + align-items: center; + gap: 1rem; + transition: all 0.3s ease; +} + +.reading-progress-bar { + flex: 1; + height: 4px; + background: rgba(255, 255, 255, 0.1); + border-radius: 2px; + overflow: hidden; + position: relative; +} + +.reading-progress-fill { + height: 100%; + background: linear-gradient(90deg, #646cff, #8ab4f8); + border-radius: 2px; + transition: width 0.3s ease; + position: relative; +} + +.reading-progress-fill.complete { + background: linear-gradient(90deg, #4ade80, #22c55e); +} + +.reading-progress-fill::after { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.3), transparent); + animation: shimmer 2s infinite; +} + +@keyframes shimmer { + 0% { transform: translateX(-100%); } + 100% { transform: translateX(100%); } +} + +.reading-progress-text { + font-size: 0.875rem; + color: #888; + font-weight: 500; + min-width: 80px; + text-align: right; +} + +.reading-progress-text:contains('✓') { + color: #4ade80; +} + +/* Hide progress indicator when not needed */ +.reading-progress-indicator.hidden { + transform: translateY(-100%); + opacity: 0; +} + +/* Mobile adjustments */ +@media (max-width: 768px) { + .reading-progress-indicator { + padding: 0.375rem 0.75rem; + gap: 0.75rem; + } + + .reading-progress-text { + font-size: 0.813rem; + min-width: 60px; + } +} + From ec34bc3d0416d5176a5c8a830a9fd01372c4e3ee Mon Sep 17 00:00:00 2001 From: Gigi Date: Mon, 13 Oct 2025 21:02:52 +0200 Subject: [PATCH 02/19] fix: position reading progress indicator at bottom of screen - Move progress indicator from top to bottom of viewport - Add box shadow for better visual separation - Update hide animation to slide up from bottom - Add padding to reader content to prevent overlap - Ensure indicator is always visible while scrolling --- src/styles/components/reader.css | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/styles/components/reader.css b/src/styles/components/reader.css index 69c726e4..d6cd0242 100644 --- a/src/styles/components/reader.css +++ b/src/styles/components/reader.css @@ -8,6 +8,7 @@ overflow: hidden; max-width: 900px; margin: 0 auto; + padding-bottom: 4rem; /* Add space for progress indicator */ } /* Video container - responsive wrapper following react-player docs */ @@ -105,18 +106,19 @@ /* Reading Progress Indicator */ .reading-progress-indicator { position: fixed; - top: 0; + bottom: 0; left: 0; right: 0; z-index: 1000; - background: rgba(26, 26, 26, 0.9); + background: rgba(26, 26, 26, 0.95); backdrop-filter: blur(8px); - border-bottom: 1px solid rgba(255, 255, 255, 0.1); + border-top: 1px solid rgba(255, 255, 255, 0.1); padding: 0.5rem 1rem; display: flex; align-items: center; gap: 1rem; transition: all 0.3s ease; + box-shadow: 0 -4px 12px rgba(0, 0, 0, 0.3); } .reading-progress-bar { @@ -170,7 +172,7 @@ /* Hide progress indicator when not needed */ .reading-progress-indicator.hidden { - transform: translateY(-100%); + transform: translateY(100%); opacity: 0; } From e921967082b26a8abf97dc5fd1f8ddca7bf471c3 Mon Sep 17 00:00:00 2001 From: Gigi Date: Mon, 13 Oct 2025 21:04:39 +0200 Subject: [PATCH 03/19] fix: move progress indicator outside reader and fix position tracking - Move ReadingProgressIndicator outside reader div for true fixed positioning - Replace position-indicator library with custom scroll tracking - Track document scroll position instead of content scroll - Remove unused position-indicator dependency - Ensure progress indicator is always visible and shows correct percentage --- package-lock.json | 7 ----- package.json | 1 - src/components/ContentPanel.tsx | 10 +++--- src/hooks/useReadingPosition.ts | 56 ++++++++++++++------------------- 4 files changed, 29 insertions(+), 45 deletions(-) diff --git a/package-lock.json b/package-lock.json index 3e2a4151..97c01592 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,7 +22,6 @@ "applesauce-relay": "^4.0.0", "date-fns": "^4.1.0", "nostr-tools": "^2.4.0", - "position-indicator": "^0.0.12", "prismjs": "^1.30.0", "react": "^18.2.0", "react-dom": "^18.2.0", @@ -8973,12 +8972,6 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/position-indicator": { - "version": "0.0.12", - "resolved": "https://registry.npmjs.org/position-indicator/-/position-indicator-0.0.12.tgz", - "integrity": "sha512-qHQejEylblB7rZ3MfXSI5hu1+Dq7EBv1BYwUIVWzJ3nZ8d6V7LFBi1zC5/XwT/01Wxddf9kaFoOy3L70/5tC+A==", - "license": "MIT" - }, "node_modules/possible-typed-array-names": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", diff --git a/package.json b/package.json index 5b459a8c..1d2be76d 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,6 @@ "applesauce-relay": "^4.0.0", "date-fns": "^4.1.0", "nostr-tools": "^2.4.0", - "position-indicator": "^0.0.12", "prismjs": "^1.30.0", "react": "^18.2.0", "react-dom": "^18.2.0", diff --git a/src/components/ContentPanel.tsx b/src/components/ContentPanel.tsx index 89b76023..d9324bff 100644 --- a/src/components/ContentPanel.tsx +++ b/src/components/ContentPanel.tsx @@ -374,8 +374,8 @@ const ContentPanel: React.FC = ({ const highlightRgb = hexToRgb(highlightColor) return ( -
- {/* Reading Progress Indicator */} + <> + {/* Reading Progress Indicator - Outside reader for fixed positioning */} {isTextContent && ( = ({ /> )} - {/* Hidden markdown preview to convert markdown to HTML */} +
+ {/* Hidden markdown preview to convert markdown to HTML */} {markdown && (
= ({

No readable content found for this URL.

)} -
+
+ ) } diff --git a/src/hooks/useReadingPosition.ts b/src/hooks/useReadingPosition.ts index 594aa9a0..775c3a08 100644 --- a/src/hooks/useReadingPosition.ts +++ b/src/hooks/useReadingPosition.ts @@ -1,19 +1,8 @@ import { useEffect, useRef, useState } from 'react' -// @ts-ignore - position-indicator types issue -import { createPositionIndicator } from 'position-indicator' - -interface ReadingPositionData { - position: number // 0 to 1 - prevPosition: number - hasUpdated: boolean - hasScroll: boolean - eventType: 'scroll' | 'resize' | 'heightChange' | 'init' - eventDate: number -} interface UseReadingPositionOptions { enabled?: boolean - onPositionChange?: (data: ReadingPositionData) => void + onPositionChange?: (position: number) => void onReadingComplete?: () => void readingCompleteThreshold?: number // Default 0.9 (90%) } @@ -26,44 +15,45 @@ export const useReadingPosition = ({ }: UseReadingPositionOptions = {}) => { const [position, setPosition] = useState(0) const [isReadingComplete, setIsReadingComplete] = useState(false) - const positionIndicatorRef = useRef(null) const hasTriggeredComplete = useRef(false) useEffect(() => { if (!enabled) return - const handleInit = (data: ReadingPositionData) => { - setPosition(data.position) - onPositionChange?.(data) - } + const handleScroll = () => { + // Get the main content area (reader content) + const readerContent = document.querySelector('.reader-html, .reader-markdown') + if (!readerContent) return - const handleUpdate = (data: ReadingPositionData) => { - setPosition(data.position) - onPositionChange?.(data) + const scrollTop = window.pageYOffset || document.documentElement.scrollTop + const windowHeight = window.innerHeight + const documentHeight = document.documentElement.scrollHeight + + // Calculate position based on how much of the content has been scrolled through + const scrollProgress = Math.min(scrollTop / (documentHeight - windowHeight), 1) + const clampedProgress = Math.max(0, Math.min(1, scrollProgress)) + + setPosition(clampedProgress) + onPositionChange?.(clampedProgress) // Check if reading is complete - if (data.position >= readingCompleteThreshold && !hasTriggeredComplete.current) { + if (clampedProgress >= readingCompleteThreshold && !hasTriggeredComplete.current) { setIsReadingComplete(true) hasTriggeredComplete.current = true onReadingComplete?.() } } - const positionIndicator = createPositionIndicator({ - onInit: handleInit, - onUpdate: handleUpdate, - useResizeListener: true, - useResizeObserver: true - }) + // Initial calculation + handleScroll() - positionIndicator.init() - positionIndicatorRef.current = positionIndicator + // Add scroll listener + window.addEventListener('scroll', handleScroll, { passive: true }) + window.addEventListener('resize', handleScroll, { passive: true }) return () => { - if (positionIndicatorRef.current) { - positionIndicatorRef.current.destroy() - positionIndicatorRef.current = null - } + window.removeEventListener('scroll', handleScroll) + window.removeEventListener('resize', handleScroll) } }, [enabled, onPositionChange, onReadingComplete, readingCompleteThreshold]) From 6a84646b0b51c3430c9413e28cbc455d98f0a3ee Mon Sep 17 00:00:00 2001 From: Gigi Date: Mon, 13 Oct 2025 21:17:11 +0200 Subject: [PATCH 04/19] chore(tailwind): setup Tailwind CSS with preflight on - Install tailwindcss, postcss, autoprefixer - Add tailwind.config.js and postcss.config.js - Create src/styles/tailwind.css with base/components/utilities - Import Tailwind before index.css in main.tsx --- package-lock.json | 79 +++++++++++++++++++++++++++++++++++++++++ package.json | 3 ++ postcss.config.js | 7 ++++ src/main.tsx | 1 + src/styles/tailwind.css | 4 +++ tailwind.config.js | 12 +++++++ 6 files changed, 106 insertions(+) create mode 100644 postcss.config.js create mode 100644 src/styles/tailwind.css create mode 100644 tailwind.config.js diff --git a/package-lock.json b/package-lock.json index 97c01592..c07dc42c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -39,9 +39,12 @@ "@typescript-eslint/eslint-plugin": "^6.14.0", "@typescript-eslint/parser": "^6.14.0", "@vitejs/plugin-react": "^4.2.1", + "autoprefixer": "^10.4.21", "eslint": "^8.55.0", "eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-refresh": "^0.4.5", + "postcss": "^8.5.6", + "tailwindcss": "^4.1.14", "typescript": "^5.2.2", "vite": "^5.0.8", "vite-plugin-pwa": "^1.0.3", @@ -4128,6 +4131,44 @@ "node": ">= 4.0.0" } }, + "node_modules/autoprefixer": { + "version": "10.4.21", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.21.tgz", + "integrity": "sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.24.4", + "caniuse-lite": "^1.0.30001702", + "fraction.js": "^4.3.7", + "normalize-range": "^0.1.2", + "picocolors": "^1.1.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, "node_modules/available-typed-arrays": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", @@ -5915,6 +5956,20 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/fraction.js": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", + "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "patreon", + "url": "https://github.com/sponsors/rawify" + } + }, "node_modules/fs-extra": { "version": "9.1.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", @@ -8533,6 +8588,16 @@ "node": "^18.17.0 || >=20.5.0" } }, + "node_modules/normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/nostr-tools": { "version": "2.17.0", "resolved": "https://registry.npmjs.org/nostr-tools/-/nostr-tools-2.17.0.tgz", @@ -9010,6 +9075,13 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, + "license": "MIT" + }, "node_modules/postcss/node_modules/nanoid": { "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", @@ -10369,6 +10441,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/tailwindcss": { + "version": "4.1.14", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.14.tgz", + "integrity": "sha512-b7pCxjGO98LnxVkKjaZSDeNuljC4ueKUddjENJOADtubtdo8llTaJy7HwBMeLNSSo2N5QIAgklslK1+Ir8r6CA==", + "dev": true, + "license": "MIT" + }, "node_modules/tar": { "version": "7.5.1", "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.1.tgz", diff --git a/package.json b/package.json index 1d2be76d..fa9faaa4 100644 --- a/package.json +++ b/package.json @@ -42,9 +42,12 @@ "@typescript-eslint/eslint-plugin": "^6.14.0", "@typescript-eslint/parser": "^6.14.0", "@vitejs/plugin-react": "^4.2.1", + "autoprefixer": "^10.4.21", "eslint": "^8.55.0", "eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-refresh": "^0.4.5", + "postcss": "^8.5.6", + "tailwindcss": "^4.1.14", "typescript": "^5.2.2", "vite": "^5.0.8", "vite-plugin-pwa": "^1.0.3", diff --git a/postcss.config.js b/postcss.config.js new file mode 100644 index 00000000..b4a6220e --- /dev/null +++ b/postcss.config.js @@ -0,0 +1,7 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} + diff --git a/src/main.tsx b/src/main.tsx index b459225a..b90b795e 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,6 +1,7 @@ import React from 'react' import ReactDOM from 'react-dom/client' import App from './App.tsx' +import './styles/tailwind.css' import './index.css' // Register Service Worker for PWA functionality diff --git a/src/styles/tailwind.css b/src/styles/tailwind.css new file mode 100644 index 00000000..a90f0749 --- /dev/null +++ b/src/styles/tailwind.css @@ -0,0 +1,4 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + diff --git a/tailwind.config.js b/tailwind.config.js new file mode 100644 index 00000000..7c7810ac --- /dev/null +++ b/tailwind.config.js @@ -0,0 +1,12 @@ +/** @type {import('tailwindcss').Config} */ +export default { + content: [ + './index.html', + './src/**/*.{ts,tsx}', + ], + theme: { + extend: {}, + }, + plugins: [], +} + From dbc0a48194135806c6c8070cd3636a8435863bde Mon Sep 17 00:00:00 2001 From: Gigi Date: Mon, 13 Oct 2025 21:18:31 +0200 Subject: [PATCH 05/19] style(global): reconcile base styles with Tailwind preflight - Add CSS variables for user-settable highlight colors - Add reading font and font size variables - Simplify global.css to work with Tailwind preflight - Remove redundant body/root styles handled by Tailwind - Keep app-specific overrides (mobile sidebar lock, loading states) --- src/styles/base/global.css | 56 +++++------------------------------ src/styles/base/variables.css | 7 +++++ 2 files changed, 14 insertions(+), 49 deletions(-) diff --git a/src/styles/base/global.css b/src/styles/base/global.css index 9b9fdd04..1fb12aac 100644 --- a/src/styles/base/global.css +++ b/src/styles/base/global.css @@ -1,68 +1,26 @@ -/* Global element styles and app container */ +/* Global element styles and app container (Tailwind-compatible) */ + +/* Body - keep only app-specific overrides */ body { - margin: 0; - min-width: 320px; - min-height: 100vh; overscroll-behavior: none; -webkit-overflow-scrolling: touch; } -/* Use dynamic viewport height if supported */ -@supports (height: 100dvh) { - body { - min-height: 100dvh; - } -} - body.mobile-sidebar-open { overflow: hidden; position: fixed; width: 100%; } -#root { - max-width: none; - margin: 0; - padding: 1rem; -} - -@media (max-width: 768px) { - #root { - padding: 0; - } -} - -.app { - text-align: center; - position: relative; -} - -.app header { - margin-bottom: 2rem; -} - -.app header h1 { - font-size: 2.5rem; - margin: 0; - color: #646cff; -} - -.app header p { - margin: 0.5rem 0 0 0; - color: #888; -} - -.loading { - text-align: center; - padding: 2rem; - color: #ccc; -} - +/* App loading states */ .loading { flex: 1; display: flex; align-items: center; justify-content: center; + text-align: center; + padding: 2rem; + color: #ccc; } diff --git a/src/styles/base/variables.css b/src/styles/base/variables.css index 9b50fef3..5cc03c67 100644 --- a/src/styles/base/variables.css +++ b/src/styles/base/variables.css @@ -16,6 +16,13 @@ --reading-font: 'Source Serif 4', serif; --reading-font-size: 18px; + + /* Highlight color variables (user-settable) */ + --highlight-color-mine: #ffff00; + --highlight-color-friends: #f97316; + --highlight-color-nostrverse: #9333ea; + --highlight-color: #ffff00; /* Default highlight color */ + /* Layout variables */ --sidebar-width: 320px; --sidebar-collapsed-width: 64px; From aab67d8375008cfd423d53ac3acf16a4106e8e1f Mon Sep 17 00:00:00 2001 From: Gigi Date: Mon, 13 Oct 2025 21:36:08 +0200 Subject: [PATCH 06/19] refactor(layout): switch to document scroll with sticky sidebars - Remove fixed container heights from three-pane layout - Desktop: sticky sidebars with max-height, document scrolls - Mobile: keep fixed overlays unchanged - Update scroll direction hook to use window scroll - Update progress indicator z-index to 1102 (above mobile overlays) - Apply Tailwind utilities to App container - Maintain responsive behavior across breakpoints --- src/App.tsx | 2 +- src/components/ThreePaneLayout.tsx | 5 +- src/styles/components/reader.css | 2 +- src/styles/layout/app.css | 75 +++++++++++++++++++----------- 4 files changed, 51 insertions(+), 33 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 24c5bae2..ac590fd7 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -238,7 +238,7 @@ function App() { -
+
diff --git a/src/components/ThreePaneLayout.tsx b/src/components/ThreePaneLayout.tsx index bb598226..28f281e9 100644 --- a/src/components/ThreePaneLayout.tsx +++ b/src/components/ThreePaneLayout.tsx @@ -98,11 +98,10 @@ const ThreePaneLayout: React.FC = (props) => { const mainPaneRef = useRef(null) // Detect scroll direction to hide/show mobile buttons - // On mobile, scroll happens in the main pane, not on window + // Now using window scroll (document scroll) instead of pane scroll const scrollDirection = useScrollDirection({ threshold: 10, - enabled: isMobile && !props.isSidebarOpen && props.isHighlightsCollapsed, - elementRef: mainPaneRef + enabled: isMobile && !props.isSidebarOpen && props.isHighlightsCollapsed }) const showMobileButtons = scrollDirection !== 'down' diff --git a/src/styles/components/reader.css b/src/styles/components/reader.css index d6cd0242..ab39a791 100644 --- a/src/styles/components/reader.css +++ b/src/styles/components/reader.css @@ -109,7 +109,7 @@ bottom: 0; left: 0; right: 0; - z-index: 1000; + z-index: 1102; /* Above mobile sidepanes (1001) and backdrop (999) */ background: rgba(26, 26, 26, 0.95); backdrop-filter: blur(8px); border-top: 1px solid rgba(255, 255, 255, 0.1); diff --git a/src/styles/layout/app.css b/src/styles/layout/app.css index 1f737d2f..5f9208f6 100644 --- a/src/styles/layout/app.css +++ b/src/styles/layout/app.css @@ -18,51 +18,64 @@ .two-pane.sidebar-collapsed { grid-template-columns: 60px 1fr; } -/* Three-pane layout */ +/* Three-pane layout - document scroll, sticky sidebars */ .three-pane { display: grid; grid-template-columns: var(--sidebar-width) 1fr var(--highlights-width); column-gap: 0; - height: calc(100vh - 2rem); transition: grid-template-columns 0.3s ease; position: relative; -} - -@supports (height: 100dvh) { - .three-pane { height: calc(100dvh - 2rem); } + min-height: 100vh; } .three-pane.sidebar-collapsed { grid-template-columns: var(--sidebar-collapsed-width) 1fr var(--highlights-width); } .three-pane.highlights-collapsed { grid-template-columns: var(--sidebar-width) 1fr var(--highlights-collapsed-width); } .three-pane.sidebar-collapsed.highlights-collapsed { grid-template-columns: var(--sidebar-collapsed-width) 1fr var(--highlights-collapsed-width); } -/* Mobile three-pane layout */ -@media (max-width: 768px) { - .three-pane { - grid-template-columns: 1fr; - grid-template-rows: 1fr; - height: 100vh; - height: 100dvh; +/* Desktop: sticky sidebars, document scroll */ +@media (min-width: 769px) { + .pane.sidebar { + position: sticky; + top: 1rem; + max-height: calc(100vh - 2rem); + overflow-y: auto; + align-self: start; + } + + .pane.main { + margin: 0 auto; + padding: 0 var(--main-horizontal-padding); + min-height: 100vh; + } + + .pane.highlights { + position: sticky; + top: 1rem; + max-height: calc(100vh - 2rem); + overflow-y: auto; + align-self: start; } - .three-pane.sidebar-collapsed, - .three-pane.highlights-collapsed, - .three-pane.sidebar-collapsed.highlights-collapsed { grid-template-columns: 1fr; } -} - -.pane.sidebar { overflow-y: auto; height: 100%; } -.pane.main { - overflow-y: auto; - height: 100%; - margin: 0 auto; - padding: 0 var(--main-horizontal-padding); - overflow-x: hidden; - contain: layout style; } /* Remove padding when sidebar is collapsed for zero gap */ .three-pane.sidebar-collapsed .pane.main { padding-left: 0; } .three-pane.sidebar-collapsed.highlights-collapsed .pane.main { padding-left: 0; } -.pane.highlights { overflow-y: auto; height: 100%; } + +/* Mobile three-pane layout */ +@media (max-width: 768px) { + .three-pane { + grid-template-columns: 1fr; + grid-template-rows: auto; + } + .three-pane.sidebar-collapsed, + .three-pane.highlights-collapsed, + .three-pane.sidebar-collapsed.highlights-collapsed { grid-template-columns: 1fr; } + + .pane.main { + margin: 0 auto; + padding: 0.5rem; + } +} /* Ensure panes are stacked in the correct order on desktop */ @media (min-width: 769px) { @@ -102,7 +115,13 @@ /* Highlights sidebar from right */ .pane.highlights { right: 0; transform: translateX(100%); } .pane.highlights.mobile-open { transform: translateX(0); box-shadow: -4px 0 12px rgba(0, 0, 0, 0.5); } - .pane.main { grid-column: 1; grid-row: 1; padding: 0.5rem; max-width: 100%; transition: opacity 0.2s ease; } + .pane.main { + grid-column: 1; + grid-row: 1; + padding: 0.5rem; + max-width: 100%; + transition: opacity 0.2s ease; + } /* Hide main content when sidepanes are open on mobile */ .three-pane .pane.main.mobile-hidden { opacity: 0; pointer-events: none; } .mobile-sidebar-backdrop { From 4f5ba9921436ae38926d04fc4da62e8f34c40099 Mon Sep 17 00:00:00 2001 From: Gigi Date: Mon, 13 Oct 2025 21:37:08 +0200 Subject: [PATCH 07/19] feat(reader): convert reading progress indicator to Tailwind - Replace CSS classes with Tailwind utilities - Use gradient backgrounds with conditional colors - Add shimmer animation to Tailwind config - Remove 80+ lines of CSS from reader.css - Maintain z-index layering (1102) above mobile overlays - Responsive design with utility classes --- src/components/ReadingProgressIndicator.tsx | 24 ++++-- src/styles/components/reader.css | 86 +-------------------- tailwind.config.js | 9 ++- 3 files changed, 26 insertions(+), 93 deletions(-) diff --git a/src/components/ReadingProgressIndicator.tsx b/src/components/ReadingProgressIndicator.tsx index bc7ef875..e772f525 100644 --- a/src/components/ReadingProgressIndicator.tsx +++ b/src/components/ReadingProgressIndicator.tsx @@ -13,17 +13,27 @@ export const ReadingProgressIndicator: React.FC = showPercentage = true, className = '' }) => { + const clampedProgress = Math.min(100, Math.max(0, progress)) + return ( -
-
+
+
+ className={`h-full rounded-sm transition-all duration-300 relative ${ + isComplete + ? 'bg-gradient-to-r from-green-400 to-green-600' + : 'bg-gradient-to-r from-indigo-500 to-blue-400' + }`} + style={{ width: `${clampedProgress}%` }} + > +
+
{showPercentage && ( -
- {isComplete ? '✓ Complete' : `${progress}%`} +
+ {isComplete ? '✓ Complete' : `${clampedProgress}%`}
)}
diff --git a/src/styles/components/reader.css b/src/styles/components/reader.css index ab39a791..992f82ed 100644 --- a/src/styles/components/reader.css +++ b/src/styles/components/reader.css @@ -103,90 +103,6 @@ .reader-header-overlay .reader-title { font-size: 1.5rem; line-height: 1.3; } } -/* Reading Progress Indicator */ -.reading-progress-indicator { - position: fixed; - bottom: 0; - left: 0; - right: 0; - z-index: 1102; /* Above mobile sidepanes (1001) and backdrop (999) */ - background: rgba(26, 26, 26, 0.95); - backdrop-filter: blur(8px); - border-top: 1px solid rgba(255, 255, 255, 0.1); - padding: 0.5rem 1rem; - display: flex; - align-items: center; - gap: 1rem; - transition: all 0.3s ease; - box-shadow: 0 -4px 12px rgba(0, 0, 0, 0.3); -} - -.reading-progress-bar { - flex: 1; - height: 4px; - background: rgba(255, 255, 255, 0.1); - border-radius: 2px; - overflow: hidden; - position: relative; -} - -.reading-progress-fill { - height: 100%; - background: linear-gradient(90deg, #646cff, #8ab4f8); - border-radius: 2px; - transition: width 0.3s ease; - position: relative; -} - -.reading-progress-fill.complete { - background: linear-gradient(90deg, #4ade80, #22c55e); -} - -.reading-progress-fill::after { - content: ''; - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.3), transparent); - animation: shimmer 2s infinite; -} - -@keyframes shimmer { - 0% { transform: translateX(-100%); } - 100% { transform: translateX(100%); } -} - -.reading-progress-text { - font-size: 0.875rem; - color: #888; - font-weight: 500; - min-width: 80px; - text-align: right; -} - -.reading-progress-text:contains('✓') { - color: #4ade80; -} - -/* Hide progress indicator when not needed */ -.reading-progress-indicator.hidden { - transform: translateY(100%); - opacity: 0; -} - -/* Mobile adjustments */ -@media (max-width: 768px) { - .reading-progress-indicator { - padding: 0.375rem 0.75rem; - gap: 0.75rem; - } - - .reading-progress-text { - font-size: 0.813rem; - min-width: 60px; - } -} +/* Reading Progress Indicator - now using Tailwind utilities in component */ diff --git a/tailwind.config.js b/tailwind.config.js index 7c7810ac..6f0fb939 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -5,7 +5,14 @@ export default { './src/**/*.{ts,tsx}', ], theme: { - extend: {}, + extend: { + keyframes: { + shimmer: { + '0%': { transform: 'translateX(-100%)' }, + '100%': { transform: 'translateX(100%)' }, + }, + }, + }, }, plugins: [], } From bd0b4e848fad1cd072ccc4a65a84294bd52d9083 Mon Sep 17 00:00:00 2001 From: Gigi Date: Mon, 13 Oct 2025 21:38:10 +0200 Subject: [PATCH 08/19] docs: update changelog with Tailwind migration progress --- CHANGELOG.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index dbd3c0e7..681d3141 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Tailwind CSS integration with preflight enabled +- Reading position tracking with visual progress indicator +- Document-level scrolling with sticky sidebars on desktop + +### Changed + +- Refactored layout system to use document scroll instead of pane scroll +- Migrated reading progress indicator to Tailwind utilities +- Simplified global CSS to work with Tailwind preflight +- Added CSS variables for user-settable theme colors + +### Fixed + +- Reading position indicator now always visible at bottom of screen +- Progress tracking now accurately reflects reading position +- Scroll behavior consistent across desktop and mobile + ## [0.5.7] - 2025-01-14 ### Added From f3f80449a6f864b72f7542ee16dca56fd85bac4f Mon Sep 17 00:00:00 2001 From: Gigi Date: Mon, 13 Oct 2025 21:58:50 +0200 Subject: [PATCH 09/19] refactor(layout): migrate mobile buttons to Tailwind utilities - Convert mobile hamburger and highlights buttons to Tailwind - Migrate mobile backdrop to Tailwind utilities - Remove 60+ lines of CSS from app.css and sidebar.css - Maintain responsive behavior and z-index layering - Keep dynamic color support for highlight button --- src/components/ThreePaneLayout.tsx | 28 +++++++++++++++++++----- src/styles/layout/app.css | 35 +----------------------------- src/styles/layout/sidebar.css | 35 +----------------------------- 3 files changed, 24 insertions(+), 74 deletions(-) diff --git a/src/components/ThreePaneLayout.tsx b/src/components/ThreePaneLayout.tsx index 28f281e9..64890d17 100644 --- a/src/components/ThreePaneLayout.tsx +++ b/src/components/ThreePaneLayout.tsx @@ -224,7 +224,15 @@ const ThreePaneLayout: React.FC = (props) => { {/* Mobile bookmark button - only show when viewing article */} {isMobile && !props.isSidebarOpen && props.isHighlightsCollapsed && ( @@ -252,7 +266,9 @@ const ThreePaneLayout: React.FC = (props) => { {/* Mobile backdrop */} {isMobile && (