Compare commits

..

1273 Commits

Author SHA1 Message Date
Gigi
fc60e6b80a chore: bump version to 0.7.3 2025-10-19 01:04:48 +02:00
Gigi
d9cdbb7279 Merge pull request #20 from dergigi/writings-controller
Make Explore non-blocking; centralize nostrverse highlights & writings controllers
2025-10-19 01:03:25 +02:00
Gigi
401d333e0f fix(explore): logged-out mode relies solely on centralized nostrverse controllers; start controllers even when logged out 2025-10-19 00:58:07 +02:00
Gigi
d32a47e3c3 perf(explore): make loading fully non-blocking; seed caches then stream and merge results progressively 2025-10-19 00:55:24 +02:00
Gigi
35efdb6d3f feat(nostrverse): add nostrverseWritingsController and subscribe in Explore; start controller at app init 2025-10-19 00:52:32 +02:00
Gigi
c7f7792d73 feat(highlights): add centralized nostrverseHighlightsController; start at app init; Explore subscribes to controller stream 2025-10-19 00:50:12 +02:00
Gigi
8aa26caae0 feat(explore): show skeletons instead of spinner; keep nostrverse non-blocking and stream into view 2025-10-19 00:48:24 +02:00
Gigi
6c00904bd5 fix(explore,nostrverse): never block explore highlights on nostrverse; show empty state instead of spinner and stream results into store immediately 2025-10-19 00:46:16 +02:00
Gigi
23526954ea fix(explore): reflect settings default scope immediately and avoid blank lists; preload/merge nostrverse from event store and keep fetches non-blocking 2025-10-19 00:42:39 +02:00
Gigi
9a437dd97b fix(explore): ensure nostrverse highlights are loaded and merged; preload nostrverse highlights at app start for instant Explore toggle 2025-10-19 00:38:05 +02:00
Gigi
0baf75462c refactor(explore): use writingsController for 'mine' posts; keep fetches non-blocking and centralized 2025-10-19 00:34:21 +02:00
Gigi
30b8f1af92 feat(writings): auto-load user writings at login so Explore 'mine' tab has local data 2025-10-19 00:30:07 +02:00
Gigi
07aea9d35f fix(explore): prevent disabling all explore scopes; ensure at least one filter remains active 2025-10-19 00:28:55 +02:00
Gigi
41a4abff37 fix(highlights): scope highlights to current article on /a and /r by deriving coordinate from naddr for early filtering, and ensure sidebar/content only show scoped highlights 2025-10-19 00:24:37 +02:00
Gigi
c9998984c3 feat(explore): include and stream my writings when enabled\n\n- Load my own writings in parallel with friends/nostrverse\n- Lazy-load on 'mine' toggle when logged in\n- Keep dedupe/sort consistent 2025-10-19 00:16:01 +02:00
Gigi
a799709e62 fix(explore): ensure writings are deduped by replaceable before visibility filtering and render 2025-10-19 00:14:20 +02:00
Gigi
18c6c3e68a fix(content): show only article-specific highlights in ContentPanel for nostr articles 2025-10-19 00:12:49 +02:00
Gigi
5e7395652f feat(explore): stream nostrverse writings when toggled on while logged in\n\n- Lazy-load nostrverse via onPost callback when filter is enabled\n- Avoid reloading twice using hasLoadedNostrverse guard\n- Keep DRY dedupe/sort behavior 2025-10-19 00:08:06 +02:00
Gigi
83076e7b01 feat(explore): stream nostrverse writings to paint instantly\n\n- Add onPost streaming callback to fetchNostrverseBlogPosts\n- Stream posts in Explore when logged out and logged in\n- Keep final deduped/sorted list after stream completes 2025-10-19 00:04:53 +02:00
Gigi
c79f4122da feat(debug): add Writings Loading section to debug page
- Add handlers for loading my writings, friends writings, and nostrverse writings
- Display writings with title, summary, author, and d-tag
- Show timing metrics (total load time and first event time)
- Use writingsController for own writings to test controller functionality
2025-10-18 23:57:46 +02:00
Gigi
179fe0bbc2 fix(explore): prevent infinite loop when loading nostrverse content
- Remove cachedHighlights, cachedWritings, myHighlights from useEffect deps
- These are derived from eventStore and caused infinite refetch loop
- Content is still seeded from cache but doesn't trigger re-fetches
2025-10-18 23:54:02 +02:00
Gigi
20b4f2b1b2 fix(explore): fetch nostrverse content when logged out
- Allow exploring nostrverse writings and highlights without account
- Default to nostrverse visibility when logged out
- Update visibility settings when login state changes
2025-10-18 23:50:12 +02:00
Gigi
936f9093cf fix(me): use myWritingsLoading state in writings tab rendering 2025-10-18 23:45:16 +02:00
Gigi
3149e5b824 feat(services): add centralized writingsController for kind 30023 2025-10-18 23:43:16 +02:00
Gigi
8619cecaf3 docs: update CHANGELOG.md for v0.7.2 2025-10-18 23:32:10 +02:00
Gigi
d40c49edb0 chore: bump version to 0.7.2 2025-10-18 23:31:25 +02:00
Gigi
ce5d97fb1f Merge pull request #19 from dergigi/loading-improvements-etc
Implement cached-first loading with EventStore
2025-10-18 23:30:46 +02:00
Gigi
ffb8031a05 feat: implement cached-first loading with EventStore across app
- Add useStoreTimeline hook for reactive EventStore queries
- Add dedupe helpers for highlights and writings
- Explore: seed highlights and writings from store instantly
- Article sidebar: seed article-specific highlights from store
- External URLs: seed URL-specific highlights from store
- Profile pages: seed other-profile highlights and writings from store
- Remove debug logging
- All data loads from cache first, then updates with fresh data
- Follows DRY principles with single reusable hook
2025-10-18 23:03:48 +02:00
Gigi
d54e1072b8 feat: load highlights from event store for instant display
- Use eventStore.timeline() to query cached highlights
- Seed Explore page with cached highlights immediately
- Provides instant display of nostrverse highlights from store
- Fresh data still fetched in background and merged
- Follows applesauce pattern with useObservableMemo
2025-10-18 22:31:59 +02:00
Gigi
55defb645c debug: prefix all nostrverse logs with [NOSTRVERSE]
- Makes it easy to filter console logs
- Updated logs in nostrverseService.ts and Explore.tsx
- All relevant logs now have consistent prefix
2025-10-18 22:25:02 +02:00
Gigi
1ba9595542 debug: add console logging for nostrverse highlights
- Log highlight counts by source (mine, friends, nostrverse)
- Log classified highlights by level
- Log visibility filter state and results
- Helps diagnose why nostrverse content isn't appearing
2025-10-18 22:22:34 +02:00
Gigi
340913f15f fix: force React to remount tab content when switching tabs
- Add key prop based on activeTab to wrapper div
- Forces complete unmount/remount of content when switching tabs
- Prevents DOM element reuse that was causing blog posts to bleed into highlights tab
2025-10-18 22:20:27 +02:00
Gigi
1d6595f754 fix: deduplicate blog posts by author:d-tag instead of event ID
- Use consistent deduplication key (author:d-tag) for replaceable events
- Prevents duplicate blog posts when same article has multiple event IDs
- Streaming updates now properly replace older versions with newer ones
- Fixes issue where same blog post card appeared multiple times
2025-10-18 22:19:17 +02:00
Gigi
6099e3c6a4 feat: store nostrverse content in centralized event store
- Add eventStore parameter to fetchNostrverseBlogPosts
- Add eventStore parameter to fetchNostrverseHighlights
- Pass eventStore from Explore component to nostrverse fetchers
- Store all nostrverse blog posts and highlights in event store
- Enables offline access to nostrverse content
2025-10-18 22:08:22 +02:00
Gigi
ed75bc6059 feat: store article-specific highlights in centralized event store
- Pass eventStore to fetchHighlightsForArticle in useBookmarksData
- Pass eventStore to fetchHighlightsForUrl in useExternalUrlLoader
- All fetched highlights now persist in the centralized event store
- Enables offline access and consistent state management
2025-10-18 22:05:22 +02:00
Gigi
dcfc08287e refactor: use centralized controllers in highlights sidebar
- Subscribe to highlightsController for user's own highlights
- Subscribe to contactsController for followed pubkeys
- Merge controller highlights with article-specific highlights
- Remove duplicate fetching logic for contacts and own highlights
- Maintain article-specific highlight fetching for context-aware display
2025-10-18 22:01:44 +02:00
Gigi
35b2168f9a fix: get initial highlights state immediately from controller
The subscription pattern only fires on *changes*, not initial state.
When Me component mounts, we need to immediately get the current
highlights from the controller, not wait for a change event.

Before:
- Subscribe to controller
- Wait for controller to emit (only happens on changes)
- Meanwhile, myHighlights stays []

After:
- Get initial state immediately: highlightsController.getHighlights()
- Then subscribe to future updates
- myHighlights is populated right away

This ensures highlights are always available when navigating to
/me/highlights, even if the controller hasn't emitted any new events.
2025-10-18 21:56:27 +02:00
Gigi
f8a9079e5f fix: don't manually set highlights in loadHighlightsTab for own profile
The real issue: loadHighlightsTab was calling setHighlights(myHighlights)
before the controller subscription had populated myHighlights, resulting
in setting highlights to an empty array.

Solution: For own profile, let the sync effect handle setting highlights.
The controller subscription + sync effect is the single source of truth.
Only fetch highlights manually when viewing other users' profiles.

Flow for own profile:
1. Controller subscription populates myHighlights
2. Sync effect (useEffect) updates local highlights state
3. No manual setting needed in loadHighlightsTab

This ensures highlights are always synced from the controller, never
from a stale/empty initial value.
2025-10-18 21:54:27 +02:00
Gigi
780996c7c5 fix: prevent "No highlights yet" flash on /me/highlights
Fix issue where "No highlights yet" message would show briefly when
navigating to /me/highlights even when user has many highlights.

Root cause:
- Sync effect only ran when myHighlights.length > 0
- Local highlights state could be empty during navigation
- "No highlights yet" condition didn't check myHighlightsLoading

Changes:
- Remove length check from sync effect (always sync myHighlights)
- Add myHighlightsLoading check to "No highlights yet" condition
- Now shows skeleton or content, never false empty state

The controller always has the highlights loaded, so we should always
sync them to local state regardless of length.
2025-10-18 21:52:25 +02:00
Gigi
809437faa6 style: make Explore a section title like Zap Splits
Add section-title class to Explore heading to match other section headings.
2025-10-18 21:49:17 +02:00
Gigi
36f14811ae refactor: add dedicated Explore section in settings
Create new ExploreSettings component and organize explore-related settings.

Changes:
- Create src/components/Settings/ExploreSettings.tsx
- Move "Default Explore Scope" from ReadingDisplaySettings to ExploreSettings
- Add ExploreSettings to Settings.tsx above Zap Splits section
- Better organization: explore settings now in dedicated section

Settings order:
1. Theme
2. Reading Display
3. Explore (new)
4. Zap Splits
5. Layout & Behavior
6. PWA
7. Relays
2025-10-18 21:47:34 +02:00
Gigi
8b95af9c49 feat: add default explore scope setting
Add user setting to control default visibility scope in /explore page.

Changes:
- Add defaultExploreScopeNostrverse/Friends/Mine to UserSettings type
- Add "Default Explore Scope" setting in ReadingDisplaySettings UI
- Update Explore component to use defaultExploreScope settings
- Set default to friends-only (nostrverse: false, friends: true, mine: false)

Users can now configure which content types (nostrverse/friends/mine)
are visible by default when visiting the explore page, separate from
the highlight visibility settings.
2025-10-18 21:45:04 +02:00
Gigi
236ade3d2f style: remove background color from explore scope filter buttons
Remove background color from .highlight-level-toggles bar in /explore page.
The visibility filter buttons (nostrverse, friends, mine) now have no
background, making the UI cleaner.
2025-10-18 21:42:53 +02:00
Gigi
c2e882ec31 refactor: simplify highlights state management - remove prop drilling
Remove unnecessary prop drilling of myHighlights/myHighlightsLoading.
Components now subscribe directly to highlightsController (DRY principle).

Changes:
- Explore: Subscribe to controller directly, no props needed
- Me: Subscribe to controller directly, no props needed
- Bookmarks: Remove myHighlights props (no longer passes through)
- App: Remove highlights state, controller manages it internally

Benefits:
-  Simpler code (no prop drilling through 3 layers)
-  More DRY (single source of truth in controller)
-  Consistent with applesauce patterns (like useActiveAccount)
-  Less boilerplate (removed ~30 lines of prop passing)
-  Controller encapsulates all state management

Pattern: Components import and subscribe to controller directly,
just like they use Hooks.useActiveAccount() or other applesauce hooks.
2025-10-18 21:39:33 +02:00
Gigi
0a382e77b9 feat: show skeleton placeholders while highlights are loading
- Pass myHighlightsLoading state from controller through App → Bookmarks → Explore/Me
- Update Explore showSkeletons logic to include myHighlightsLoading
- Update Me showSkeletons logic to include myHighlightsLoading for own profile
- Sync myHighlights to Me component via useEffect for real-time updates
- Remove highlightsController import from Me (now uses props)

Benefits:
- Better UX with skeleton placeholders instead of empty/spinner states
- Consistent loading experience across Explore and Me pages
- Clear visual feedback when highlights are loading from controller
- Smooth transition from skeleton to actual content
2025-10-18 21:34:44 +02:00
Gigi
a1fd4bfc94 feat: use highlights controller in Explore page
- Pass myHighlights from controller through App.tsx → Bookmarks → Explore
- Merge controller highlights with friends/nostrverse highlights
- Seed Explore with myHighlights immediately (no re-fetch needed)
- Eliminate redundant fetching of user's own highlights
- Improve performance and consistency across the app

Benefits:
- User's highlights appear instantly in /explore (already loaded)
- No duplicate fetching of same data
- DRY principle - single source of truth for user highlights
- Better offline support (highlights from controller are in event store)
2025-10-18 21:28:42 +02:00
Gigi
530cc20cba feat: implement centralized highlights controller
- Create highlightsController with subscription API and event store integration
- Auto-load user highlights on app start (alongside bookmarks and contacts)
- Store highlight events in applesauce event store for offline support
- Update Me.tsx to use controller for own profile highlights
- Add optional eventStore parameter to all highlight fetch functions
- Pass eventStore through Debug component for persistent storage
- Implement incremental sync with localStorage-based lastSyncedAt tracking
- Add generation-based cancellation for in-flight requests
- Reset highlights on logout

Closes #highlights-controller
2025-10-18 21:19:57 +02:00
Gigi
a275c0a8e3 refactor: consolidate bookmarks and contacts auto-loading
- Combine both auto-load effects into single useEffect
- Load bookmarks and contacts together when account is ready
- Keep code DRY - same pattern, same timing, same place
- Both use their respective controllers
- Both check loading state before triggering
2025-10-18 21:03:01 +02:00
Gigi
cb43b748e4 temp: disable auto-load of contacts for testing
- Comment out contacts state and subscriptions
- Comment out auto-load effect
- Allows manual testing of contact loading in Debug page
- Remember to re-enable after testing
2025-10-18 20:59:14 +02:00
Gigi
ff9ce46448 refactor: simplify friends highlights loading to use cached contacts
- Remove redundant contact loading check
- Directly use contacts from centralized controller
- App.tsx already auto-loads contacts on login
- Clearer message indicating cached contacts are being used
- Faster execution since no contact loading needed
2025-10-18 20:58:14 +02:00
Gigi
1e6718fe1e fix: improve Load Friends button behavior in Debug
- Add local loading state for button (friendsButtonLoading)
- Clear friends list before loading to show streaming
- Set final result after controller completes
- Add error handling and logging
- Remove unused global friendsLoading subscription
- Button now properly shows loading state and results
2025-10-18 20:56:53 +02:00
Gigi
d6a913f2a6 feat: add centralized contacts controller
- Create contactsController similar to bookmarkController
- Manage friends/contacts list in one place across the app
- Auto-load contacts on login, cache results per pubkey
- Stream partial contacts as they arrive
- Update App.tsx to subscribe to contacts controller
- Update Debug.tsx to use centralized contacts instead of fetching directly
- Reset contacts on logout
- Contacts won't reload unnecessarily (cached by pubkey)
- Debug 'Load Friends' button forces reload to show streaming behavior
2025-10-18 20:51:03 +02:00
Gigi
8030e2fa00 feat: make friends highlights loading non-blocking
- Start fetching highlights immediately when partial contacts arrive
- Track seen authors to avoid duplicate queries
- Fire-and-forget pattern for partial fetches (like bookmark loading)
- Only await final batch for remaining authors
- Highlights stream in progressively as contacts are discovered
- Matches the non-blocking pattern used in Explore.tsx and bookmark loading
2025-10-18 20:47:35 +02:00
Gigi
1ff2f28566 chore: increase nostrverse highlights limit from 100 to 500
- Better for testing and debugging with more realistic data volumes
2025-10-18 20:43:37 +02:00
Gigi
78457335c6 refactor: simplify nostrverse highlights loading to direct query
- Use direct queryEvents with kind:9802 filter instead of service wrapper
- Add streaming with onEvent callback for immediate UI updates
- Track first event timing for performance analysis
- Remove unused fetchNostrverseHighlights import
2025-10-18 20:43:02 +02:00
Gigi
553feb10df feat: add debug buttons for highlight loading and Web of Trust
- Add three quick-load buttons: Load My Highlights, Load Friends Highlights, Load Nostrverse Highlights
- Add Web of Trust section with Load Friends button to display followed npubs
- Stream highlights with dedupe and timing metrics
- Display friends count and scrollable list of npubs
- All buttons respect loading states and account requirements
2025-10-18 20:39:57 +02:00
Gigi
ba5d7df3bd fix: show highlight button for all reading content
- Show highlight button when readerContent exists (both nostr articles and external URLs)
- Hide highlight button when browsing app pages like explore, settings, etc.
- Ensures highlighting is available for all readable content but not for navigation pages
2025-10-18 20:26:11 +02:00
Gigi
cf3ca2d527 feat: show highlight button only when viewing articles
- Only display the floating highlight button when currentArticle exists or selectedUrl is a nostr article
- Prevents highlight button from showing on external URLs, videos, or other content types
- Improves UX by showing highlight functionality only where it's relevant
2025-10-18 20:23:45 +02:00
Gigi
06763d5307 fix: resolve unused variable linting error in Debug.tsx 2025-10-18 20:23:18 +02:00
Gigi
a08e4fdc24 chore: bump version to 0.7.1 2025-10-18 20:22:03 +02:00
Gigi
bc7b4ae42d feat(debug): add time-to-first-event tracking for bookmarks
- Track and display time to first bookmark event arrival
- Mirror highlight loading metrics for consistency
- Shows how quickly local/fast relays respond
- Renamed 'load' stat to 'total' for clarity
- Clear first event timing on reset
2025-10-18 20:20:59 +02:00
Gigi
4dc1894ef3 feat(debug): default highlight loading to logged-in user
- Author mode now defaults to current user's pubkey if not specified
- Changed default mode from 'article' to 'author' for better UX
- Updated placeholder to show logged-in user's pubkey
- Updated description to clarify default behavior
- Makes 'Load Highlights' button immediately useful without input
2025-10-18 20:19:53 +02:00
Gigi
f00f26dfe0 feat(debug): add Highlight Loading section with streaming metrics
- Add query mode selector (Article/#a, URL/#r, Author)
- Stream highlight events as they arrive with onEvent callback
- Track timing metrics: total load time and time-to-first-event
- Display highlight summaries with content, tags, and metadata
- Support EOSE-based completion via queryEvents helper
- Mirror bookmark loading section UX for consistency
2025-10-18 10:05:56 +02:00
Gigi
2e59bc9375 feat(highlights): add optional session cache with TTL
- Add in-memory cache with 60s TTL for article/url/author queries
- Check cache before network fetch to reduce redundant queries
- Support force flag to bypass cache when needed
- Stream cached results through onHighlight callback for consistency
2025-10-18 10:04:13 +02:00
Gigi
0d50d05245 feat(highlights): refactor fetchers to use EOSE-based queryEvents
- Replace ad-hoc Rx timeout-based queries with centralized queryEvents helper
- Remove artificial timeouts (1200ms/6000ms) in favor of EOSE signals
- Use KINDS.Highlights consistently instead of hardcoded 9802
- Maintain streaming callbacks for instant UI updates
- Parallel queries for article #a and #e tags
- Local-first relay prioritization via queryEvents
2025-10-18 10:03:13 +02:00
Gigi
90c74a8e9d docs: update CHANGELOG.md for v0.7.0 2025-10-18 09:50:23 +02:00
Gigi
a4bad34a90 chore: bump version to 0.7.0 2025-10-18 09:48:48 +02:00
Gigi
84ff24e06a Merge pull request #18 from dergigi/bunker-support
feat: add bunker authentication, progressive bookmarks, and debug page
2025-10-18 09:48:08 +02:00
Gigi
aaf8a9d4fc fix: increase PWA cache limit to 3 MiB for larger bundles 2025-10-18 09:47:05 +02:00
Gigi
efa6d13726 feat: improve bunker error message formatting
- Add 'Failed:' prefix to error messages
- Add line breaks between error and signer suggestions
- Clearer visual separation of error and help text
2025-10-18 09:40:29 +02:00
Gigi
6116dd12bc feat: hide bookmark controls when logged out
- Only show heart/support button when logged out
- Hide refresh, grouping, and view mode buttons when not logged in
- Cleaner, simpler footer for logged out state
2025-10-18 09:37:17 +02:00
Gigi
210cdd41ec feat: rename Bunker button to Signer
Change button label from 'Bunker' to 'Signer' for better clarity and user-friendliness
2025-10-18 09:35:28 +02:00
Gigi
9378b3c9a9 feat: left-align error message text
- Add text-align: left to login-error
- Change align-items to flex-start for better multi-line text alignment
- Icon now aligns to top instead of center
2025-10-18 09:35:04 +02:00
Gigi
973409e82a feat: add signer suggestions to bunker URI validation error
- Show Amber and Aegis links when bunker URI format is invalid
- Consistent helpful messaging across all bunker errors
- Helps users even when they don't have the right format
2025-10-18 09:34:25 +02:00
Gigi
5d6f48b9a8 feat: add signer suggestions to bunker error messages
- Show helpful message when bunker connection fails
- Suggest Amber (Android) and Aegis (iOS) signers with links
- Links: Amber GitHub and Aegis TestFlight
- Similar pattern to extension error message
2025-10-18 09:33:18 +02:00
Gigi
4921427ad4 feat: simplify login description text
Change 'Connect your nostr npub' → 'Connect your npub'
npub is already nostr-specific, so 'nostr' is redundant
2025-10-18 09:23:20 +02:00
Gigi
ad8cad29d3 fix: ensure bunker input stays centered and constrained
- Add width: 100% to bunker-input-container and bunker-input
- Add box-sizing: border-box to properly calculate width with padding
- Prevents bunker dialog from extending beyond centered layout
2025-10-18 09:22:49 +02:00
Gigi
8d4a4a04a3 fix: catch 'Signer extension missing' error message
- Add check for 'Signer extension missing' error
- Add case-insensitive check for 'extension missing'
- Ensure nos2x link is shown when no extension is found
2025-10-18 09:21:45 +02:00
Gigi
1dc44930b4 feat: make error message links more obvious
- Add primary color and underline to links in error messages
- Increase font weight to 600 for better visibility
- Add hover state with color transition
- nos2x link now clearly stands out as clickable
2025-10-18 09:21:05 +02:00
Gigi
c77907f87a feat: improve extension login error messages
- Show specific message when no extension is found
- Show message when authentication is cancelled/denied
- Display actual error message for other failures
- Remove generic 'Login failed' message
2025-10-18 09:20:33 +02:00
Gigi
9345228e66 feat: add nos2x link to extension error message
- Update error message to mention 'like nos2x'
- Add clickable link to nos2x Chrome Web Store
- Change error type to support React nodes for richer messages
2025-10-18 09:19:20 +02:00
Gigi
811362175c feat: hide Extension button when Bunker input is shown
- Extension button now hidden when bunker input is visible
- Reduces visual clutter and confusion
- Clear focus on the active login method
2025-10-18 09:17:44 +02:00
Gigi
3d22e7a3cb feat: simplify button text to single words
- 'Extension Login' → 'Extension'
- 'Bunker Login' → 'Bunker'
Icons + context make the action clear, minimalist approach
2025-10-18 09:16:52 +02:00
Gigi
0b0d3c2859 feat: use nostr-native language in login description
Change 'Login to see' → 'Connect your nostr npub to see'
More specific and aligned with nostr terminology
2025-10-18 09:16:18 +02:00
Gigi
1f8d18071c feat: shorten login button text for cleaner UI
- 'Login with Extension' → 'Extension Login'
- 'Login with Bunker' → 'Bunker Login'
More concise and easier to scan
2025-10-18 09:15:13 +02:00
Gigi
a4afe59437 fix: properly display FontAwesome icons in login buttons
- Import and use FontAwesomeIcon component from @fortawesome/react-fontawesome
- Add puzzle piece icon (faPuzzlePiece) for Extension button
- Add shield icon (faShieldHalved) for Bunker button
- Add info circle icon (faCircleInfo) for error messages
- Update CSS to properly style SVG icons with correct sizing
2025-10-18 09:14:45 +02:00
Gigi
1fe3786a3d feat: update login title to be more personable
Change 'Welcome to Boris' to 'Hi! I'm Boris.' for a friendlier, more welcoming first impression
2025-10-18 09:13:05 +02:00
Gigi
42d265731f feat: hide login button and user icon when logged out
- Remove redundant login button from sidebar header
- Hide profile avatar when no active account
- Users can now only login through the main login screen
- Logout button only shown when logged in
- Clean up unused imports (useState, Accounts, faRightToBracket)
2025-10-18 09:12:22 +02:00
Gigi
e4b4b97874 feat: highlight 'your own highlights' in login copy
- Style 'your own highlights' text with user's mine highlight color
- Uses --highlight-color-mine CSS variable from settings
- Adds subtle padding and border-radius for clean highlight effect
2025-10-18 09:11:04 +02:00
Gigi
1870c307da feat: improve login UI with better copy and modern design
- Add welcoming title 'Welcome to Boris'
- Update description to highlight key features (bookmarks, long-form articles, highlights)
- Change button labels to 'Login with Extension' and 'Login with Bunker'
- Add FontAwesome icons to login buttons
- Create dedicated login.css with modern, mobile-first styling
- Improve bunker input UI with better spacing and visual hierarchy
- Use softer error styling with amber/warning colors instead of harsh red
- Add smooth transitions and hover effects
- Ensure mobile-optimized touch targets
2025-10-18 09:10:14 +02:00
Gigi
bcb6cfbe97 feat: auto-load bookmarks on login and page mount
Added centralized auto-loading effect that handles all scenarios:
- User logs in (activeAccount becomes available)
- Page loads with existing session
- User logs out and back in (bookmarks cleared by reset)

Watches activeAccount and relayPool, triggers when both ready and no
bookmarks loaded yet. Handles all login methods (extension, bunker) via
single reactive effect.
2025-10-18 01:05:29 +02:00
Gigi
6ba1ce27b7 fix: add extraRelays to EventLoader and AddressLoader
The loaders were initialized without extraRelays, so they had no relays
to fetch from. Added RELAYS config as extraRelays option for both loaders.

This ensures the loaders know where to query for events when hydrating
bookmarks in the background.
2025-10-18 00:58:34 +02:00
Gigi
2f620265f4 chore: clean up verbose debug logging in hydration methods
Removed excessive per-event logging from EventLoader and AddressLoader
subscriptions. Keep only essential logs:
- Initial hydration count
- Error logging

This reduces console noise while maintaining visibility into hydration
progress and errors.
2025-10-18 00:54:56 +02:00
Gigi
61ae31c6a2 refactor: replace manual batching with applesauce EventLoader and AddressLoader
Replaced manual queryEvents batching with applesauce built-in loaders.

Key Changes:
- EventLoader for regular events (by ID) - auto-batches and streams
- AddressLoader for addressable events (coordinates) - handles kind batching
- Added EventStore instance to BookmarkController
- Initialize loaders in start() method
- hydrateByIds and hydrateByCoordinates now synchronous
- Removed manual chunk, IDS_BATCH_SIZE, etc.

Benefits:
- Follows applesauce best practices from examples
- More reliable (no manual timeout logic)
- Better performance (intelligent batching)
- Streaming results (progressive updates)
- Built-in deduplication via EventStore

Pattern: merge pointers through loader, subscribe to stream results.
2025-10-18 00:54:18 +02:00
Gigi
b0fcb0e897 debug: add detailed logging to diagnose hydration hanging
Added extensive logging to track queryEvents lifecycle:
- Log when queryEvents is called
- Log each event as it's received via onEvent callback
- Log when batch completes with event count
- Log errors if batch fails

This will help identify where the hydration is hanging - whether:
- queryEvents never returns
- No events are received
- Some batches fail silently

No functional changes, only diagnostic logging.
2025-10-18 00:49:05 +02:00
Gigi
3b08cd5d23 fix: remove setName filter from Amethyst bookmark grouping
Fixed issue where 489 kind:30001 bookmarks were not appearing in groups
because they had setName: undefined instead of setName: 'bookmark'.

Changes:
- Removed setName === 'bookmark' requirement from amethystPublic/Private filters
- Now all kind:30001 bookmarks are grouped correctly regardless of setName
- Removed debug logging that was added to diagnose the issue

Before: Only 40 bookmarks shown (26 NIP-51 + 7 standalone + 7 web)
After: All 522 bookmarks shown (26 NIP-51 + 489 Amethyst + 7 web)
2025-10-18 00:44:54 +02:00
Gigi
a3a00b8456 debug: add setName distribution logging for kind:30001 bookmarks
Added console log to show the distribution of setName values for all
kind:30001 bookmarks. This will help diagnose why 489 Amethyst bookmarks
aren't appearing in the amethystPublic/amethystPrivate groups.

Expected to see setName='bookmark' but need to verify what values are
actually present in the data.
2025-10-18 00:43:18 +02:00
Gigi
7fecc0c0c3 debug: add logging to diagnose bookmark grouping issue
Added console logs in groupIndividualBookmarks to show:
- Distribution of sourceKind values across all bookmarks
- Sample items with their sourceKind, isPrivate, setName, and id
- Count of items in each group after filtering

This will help identify why grouped view shows only ~40 bookmarks
while flat view shows 500+.
2025-10-18 00:36:40 +02:00
Gigi
93d0284fd6 feat: implement batched background hydration for bookmarks
Implemented efficient background event fetching with:

1. Batching constants:
- IDS_BATCH_SIZE = 100 (regular events)
- D_TAG_BATCH_SIZE = 50 (identifiers)
- AUTHORS_BATCH_SIZE = 50 (authors)

2. Utility functions:
- chunk<T>(arr, size) - split arrays into batches
- hydrationGeneration field - cancellation token

3. Two hydration methods:
- hydrateByIds: Fetches events by ID in batches of 100
- hydrateByCoordinates: Fetches addressable events by kind with 50×50 author×id batches

4. Progressive updates:
- Emit bookmarks instantly with placeholders (IDs only)
- Re-emit after each event arrives via onEvent callback
- All hydration runs in background (fire-and-forget)

5. Cancellation support:
- Increment hydrationGeneration on reset()/start()
- All hydration loops check generation and exit if changed
- Cleanly cancels in-flight fetching when user reloads

Benefits:
- No more hanging with 400+ bookmarked events
- Progressive UI updates as metadata loads
- Efficient relay usage with batched queries
- Clean cancellation on navigation/reload

All bookmarks appear instantly, titles/content hydrate progressively.
2025-10-18 00:34:26 +02:00
Gigi
94d5089e33 docs: clarify Amethyst bookmark structure in Amber.md
Updated documentation to explicitly state that:
- Amethyst bookmarks are stored in a SINGLE kind:30001 event with d-tag 'bookmark'
- This one event contains BOTH public (in tags) and private (in encrypted content) bookmarks
- When processed, it produces separate items with different isPrivate flags
- Example: 76 public + 416 private = 492 total bookmarks from one event

Added sections on:
- Event structure with d-tag requirement
- Processing flow showing how items are tagged
- UI grouping logic with setName check
- Why both public and private come from the same event
2025-10-18 00:22:23 +02:00
Gigi
5965bc1747 fix: check d-tag bookmark for Amethyst grouping 2025-10-18 00:21:33 +02:00
Gigi
0fbf80b04f chore: remove debug logging from bookmark grouping
Removed temporary console.log statements added for debugging. The issue has been identified and fixed - bookmarks were being filtered out by hasContent() when they only had IDs.
2025-10-18 00:18:33 +02:00
Gigi
2004ce76c9 fix: show bookmarks even when they only have IDs (no content yet)
Root cause: hasContent() was filtering out bookmarks that didn't have content text yet. When we skip event fetching for large collections (>100 IDs), bookmarks only have IDs as placeholders, causing 511/522 bookmarks to be filtered out.

Solution: Updated hasContent() to return true if bookmark has either:
- Valid content (original behavior)
- OR a valid ID (placeholder until events are fetched)

This allows all 522 bookmarks to appear in the sidebar immediately, showing IDs/URLs as placeholders until full event data loads.

Removed debug logging from bookmarkUtils as it served its purpose.
2025-10-18 00:18:13 +02:00
Gigi
90c79e34eb debug: add logging to bookmark grouping to diagnose missing bookmarks
Added console logs to groupIndividualBookmarks to see:
- Total items being grouped
- Count per group (nip51Public, nip51Private, amethystPublic, amethystPrivate, standaloneWeb)
- Sample of first 3 items with their sourceKind and isPrivate properties

This will help diagnose why 532 bookmarks are emitted but not appearing in sidebar.
2025-10-18 00:16:34 +02:00
Gigi
6ea0fd292c fix: skip background event fetching when there are too many IDs
Problem: With 400+ bookmarked events, trying to fetch all referenced events at once caused queryEvents to hang/timeout, making bookmarks appear to not load even though they were emitted.

Solution:
- Added MAX_IDS_TO_FETCH limit (100 IDs)
- Added MAX_COORDS_TO_FETCH limit (100 coordinates)
- If counts exceed limits, skip fetching and show bookmarks with IDs only
- Bookmarks still appear immediately with placeholder data (IDs)
- For smaller collections, metadata still loads in background

This fixes the hanging issue for users with large bookmark collections - all 532 bookmarks will now appear instantly in the sidebar (showing IDs), without waiting for potentially slow/hanging queryEvents calls.
2025-10-18 00:14:33 +02:00
Gigi
193c1f45d4 fix: include decrypted private bookmarks in sidebar
Root cause: When decryption completed, we were only storing counts, not the actual decrypted bookmark items. When buildAndEmitBookmarks ran, it would try to decrypt again or skip encrypted events entirely.

Changes:
- Renamed decryptedEvents to decryptedResults and changed type to store actual IndividualBookmark arrays
- Store full decrypted results (publicItems, privateItems, metadata) when decryption completes
- In buildAndEmitBookmarks, separate unencrypted and decrypted events
- Process only unencrypted events with collectBookmarksFromEvents
- Merge in stored decrypted results for encrypted events
- Updated filter to check decryptedResults map for encrypted events

This fixes the missing Amethyst bookmarks issue - all 416 private items should now appear in the sidebar after decryption completes.
2025-10-18 00:11:17 +02:00
Gigi
4da3a0347f feat: add bookmark grouping toggle (grouped by source vs flat chronological)
Changes:
- Updated groupIndividualBookmarks to group by source kind (10003, 30001, 39701) instead of content type
- Added toggle button in bookmark footer to switch between grouped and flat views
- Default mode is 'grouped by source' showing: My Bookmarks, Private Bookmarks, Amethyst Lists, Web Bookmarks
- Flat mode shows single 'All Bookmarks (X)' section sorted chronologically
- Preference persists to localStorage
- Implemented in both BookmarkList.tsx and Me.tsx

Files modified:
- src/utils/bookmarkUtils.tsx - New grouping logic
- src/components/BookmarkList.tsx - Added state, toggle button, conditional sections
- src/components/Me.tsx - Added state, toggle button, conditional sections
2025-10-17 23:55:15 +02:00
Gigi
795ef5016e feat: implement fully progressive, non-blocking bookmark loading
Changes:
- Emit bookmarks IMMEDIATELY with placeholders (IDs only)
- Fetch referenced events in background (non-blocking)
- Re-emit progressively as events load:
  1. First emit: IDs only (instant)
  2. Second emit: after fetching events by ID
  3. Third emit: after fetching addressable events

This solves the hanging issue by:
- Never blocking the initial display
- Making all event fetching happen in background Promises
- Updating the UI progressively as metadata loads

Sidebar will show bookmarks instantly with IDs, then titles/content will populate as events arrive.
2025-10-17 23:40:39 +02:00
Gigi
83693f7fb0 fix: skip event fetching to unblock sidebar population
Root cause: queryEvents() hangs when fetching referenced events by ID
Temporary fix: Skip event fetching entirely, show bookmark items without full metadata

The logs showed:
- [bookmark] 🔧 Fetching events by ID...
- (never completes, hangs indefinitely)

This blocked buildAndEmitBookmarks from completing and emitting to the sidebar.

TODO: Investigate why queryEvents with { ids: [...] } doesn't complete/timeout
2025-10-17 23:36:51 +02:00
Gigi
c55e20f341 debug: add granular logging to track buildAndEmitBookmarks flow
Added logging at every step of buildAndEmitBookmarks:
- After collectBookmarksFromEvents returns
- Before/after fetching events by ID
- Before/after fetching addressable events
- Before/after hydration and dedup
- Before/after enrichment and sorting
- Before creating final Bookmark object

This will show exactly where the process is hanging.
2025-10-17 23:34:28 +02:00
Gigi
1430d2fc47 refactor: use [bookmark] prefix for all bookmark logs
Changed all console logs to use [bookmark] prefix:
- Controller: all logs now use [bookmark] instead of [controller]
- App: all bookmark-related logs use [bookmark] instead of [app]

This allows filtering console with 'bookmark' to see only relevant logs for bookmark loading/debugging.
2025-10-17 23:32:10 +02:00
Gigi
3f24ccff74 debug: add detailed error logging to buildAndEmitBookmarks
Added logging at each step:
- Before calling collectBookmarksFromEvents
- After collectBookmarksFromEvents returns
- Detailed error info if it fails (message + stack)

This will show us exactly where the silent failure is happening.
2025-10-17 23:28:38 +02:00
Gigi
51b7e53385 debug: add extensive logging to track bookmark flow
Simplified to only show unencrypted bookmarks:
- Skip encrypted events entirely (no decrypt for now)
- This eliminates all parse errors

Added comprehensive logging:
- Controller: log when building, how many items, how many listeners, when emitting
- App: log when subscribing, when receiving bookmarks, when loading state changes

This will help identify where the disconnect is between controller and sidebar.
2025-10-17 23:27:04 +02:00
Gigi
8dbb18b1c8 fix: only build bookmarks from ready events (unencrypted or decrypted)
Filter events in buildAndEmitBookmarks to avoid parse errors:
- Unencrypted events: always included
- Encrypted events: only included if already decrypted

Progressive flow:
- Unencrypted event arrives → build bookmarks immediately
- Encrypted event arrives → wait for decrypt → then build bookmarks
- Each build only processes ready events (no parse errors)

Sidebar now populates with unencrypted bookmarks immediately, encrypted ones appear after decrypt.
2025-10-17 23:24:17 +02:00
Gigi
88bc7f690e feat: add progressive bookmark updates via callback pattern
Changed bookmark controller to emit updates progressively:
- Unencrypted events: immediate buildAndEmitBookmarks call
- Encrypted events: buildAndEmitBookmarks after decrypt completes
- Each update emits new bookmark list to subscribers

Removed coalescing/scheduling logic (scheduleBookmarkUpdate):
- Direct callback pattern is simpler and more predictable
- Updates happen exactly when events are ready

Progressive sidebar population now works correctly without parse errors.
2025-10-17 23:19:32 +02:00
Gigi
29ef21a1fa fix: restore Debug page decrypt display via onDecryptComplete callback
Added onDecryptComplete callback to controller:
- Controller emits decrypt results (eventId, publicCount, privateCount)
- Debug subscribes to see decryption progress
- setDecryptedEvents updated with decrypt results for UI display

Debug page now shows decrypted content counts for encrypted bookmark lists (like kind:30001 Amethyst-style NIP-04 bookmarks).
2025-10-17 23:14:10 +02:00
Gigi
7a75982715 fix: make controller onEvent non-blocking for queryEvents completion
Changed onEvent callback from async to synchronous:
- Removed await inside onEvent that was blocking observable
- Decryption now fires in background using .then()/.catch()
- Allows queryEvents to complete (EOSE) and trigger final bookmark build

This matches the working Debug pattern and allows bookmarks to appear in sidebar.
2025-10-17 23:12:19 +02:00
Gigi
f95f8f4bf1 refactor: remove deprecated bookmark service files
Deleted bookmarkService.ts and bookmarkStream.ts:
- All functionality now consolidated in bookmarkController.ts
- No more duplication of streaming/decrypt logic
- Single source of truth for bookmark loading
2025-10-17 23:09:23 +02:00
Gigi
9eef5855a9 feat: create shared bookmark controller for Debug-driven loading
Created bookmarkController.ts singleton:
- Encapsulates Debug's working streaming/decrypt logic
- API: start(), onRawEvent(), onBookmarks(), onLoading(), reset()
- Live deduplication, sequential decrypt, progressive updates

Updated App.tsx:
- Removed automatic loading triggers (useEffect)
- Subscribe to controller for bookmarks/loading state
- Manual refresh calls controller.start()

Updated Debug.tsx:
- Uses controller.start() instead of internal loader
- Subscribes to onRawEvent for UI display (unchanged)
- Pressing 'Load Bookmarks' now populates app sidebar

No automatic loads on login/mount. App passively receives updates from Debug-driven controller.
2025-10-17 23:08:36 +02:00
Gigi
2e70745bab fix: make bookmarksLoading optional in Me component
Made bookmarksLoading prop optional in MeProps since it's not currently used.
Reserved for future use when we want to show centralized loading state.

All linting and type checks now pass.
2025-10-17 22:52:16 +02:00
Gigi
8a971dfe52 refactor: pass bookmarks as props to Me component
Updated Me.tsx to receive bookmarks from centralized App state:
- Added bookmarks and bookmarksLoading to MeProps
- Removed local bookmarks state
- Removed bookmark caching (now handled at App level)

Updated Bookmarks.tsx to pass bookmarks props to Me component:
- Both 'me' and 'profile' views receive centralized bookmarks

All bookmark data now flows from App.tsx -> Bookmarks.tsx -> Me.tsx with no duplicate fetching or local state.
2025-10-17 22:50:07 +02:00
Gigi
a004e96eca feat: extract bookmark streaming helpers and centralize loading
Created bookmarkStream.ts with shared helpers:
- getEventKey: deduplication logic
- hasEncryptedContent: encryption detection
- loadBookmarksStream: streaming with non-blocking decryption

Refactored bookmarkService.ts to use shared helpers:
- Uses loadBookmarksStream for consistent behavior with Debug page
- Maintains progressive loading via callbacks
- Added accountManager parameter to fetchBookmarks

Updated App.tsx to pass accountManager to fetchBookmarks:
- Progressive loading indicators via onProgressUpdate callback

All bookmark loading now uses the same battle-tested streaming logic as Debug page.
2025-10-17 22:47:20 +02:00
Gigi
ce2432632c refactor: consolidate bookmark loading into single centralized function
Removed duplicate bookmark loading logic from Debug page:
- Debug 'Load Bookmarks' button now calls centralized onRefreshBookmarks
- Removed redundant state (bookmarkEvents, bookmarkStats, decryptedEvents)
- Removed unused helper functions (getKindName, getEventSize, etc.)
- Cleaned up imports (Helpers, queryEvents, collectBookmarksFromEvents)
- Simplified UI to show timing only, bookmarks visible in sidebar

Now there's truly ONE place for bookmark loading (bookmarkService.ts),
called from App.tsx and used throughout the app. Debug page's button
is now the same as clicking refresh in the bookmark sidebar.
2025-10-17 22:28:35 +02:00
Gigi
56b3100c8e fix: correct TypeScript types in Debug component
Fixed type error in Debug.tsx:
- Changed highlightVisibility from string to proper HighlightVisibility object
- Used 'support' prop instead of invalid 'children' prop for ThreePaneLayout
- Set showSupport={true} to properly render debug content

All linting and type checks now pass.
2025-10-17 22:19:09 +02:00
Gigi
327d65a128 feat: add bookmarks sidebar to Debug page
Added ThreePaneLayout to Debug page so bookmarks are visible:
- Debug page now has same layout as other pages
- Shows bookmarks sidebar on the left
- Debug content in the main pane
- Can compare centralized app bookmarks with Debug bookmarks side-by-side

This makes it easy to verify that centralized bookmark loading
works the same as the Debug page implementation.
2025-10-17 22:17:11 +02:00
Gigi
e5a7a07deb fix: bookmark loading completing properly now
Fixed critical issue where async operations in onEvent callback
were blocking the queryEvents observable from completing:

Changes:
1. Removed async/await from onEvent callback
   - Now just collects events synchronously
   - No blocking operations in the stream

2. Moved auto-decryption to after query completes
   - Batch process encrypted events after EOSE
   - Sequential decryption (cleaner, more predictable)

3. Simplified useEffect triggers in App.tsx
   - Removed duplicate mount + account change effects
   - Single effect handles both cases

Result: Query now completes properly, bookmarks load and display.
2025-10-17 22:13:58 +02:00
Gigi
5bd57573be debug: add detailed logging for bookmark loading
Added comprehensive console logs to diagnose bookmark loading issue:
- [app] prefix for all bookmark-related logs
- Log account pubkey being used
- Log each event as it arrives
- Log auto-decrypt attempts
- Log final processing steps
- Log when no bookmarks found

This will help identify where the bookmark loading is failing.
2025-10-17 22:11:47 +02:00
Gigi
c2223e6b08 feat: centralize bookmark loading with streaming and auto-decrypt
Implemented centralized bookmark loading system:
- Bookmarks loaded in App.tsx with streaming + auto-decrypt pattern
- Load triggers: login, app mount, manual refresh only
- No redundant fetching on route changes

Changes:
1. bookmarkService.ts: Refactored fetchBookmarks for streaming
   - Events stream with onEvent callback
   - Auto-decrypt encrypted content (NIP-04/NIP-44) as events arrive
   - Progressive UI updates during loading

2. App.tsx: Added centralized bookmark state
   - bookmarks and bookmarksLoading state in AppRoutes
   - loadBookmarks function with streaming support
   - Load on mount if account exists (app reopen)
   - Load when activeAccount changes (login)
   - handleRefreshBookmarks for manual refresh
   - Pass props to all Bookmarks components

3. Bookmarks.tsx: Accept bookmarks as props
   - Receive bookmarks, bookmarksLoading, onRefreshBookmarks
   - Pass onRefreshBookmarks to useBookmarksData

4. useBookmarksData.ts: Simplified to accept bookmarks as props
   - Removed bookmark fetching logic
   - Removed handleFetchBookmarks function
   - Accept onRefreshBookmarks callback
   - Use onRefreshBookmarks in handleRefreshAll

5. Me.tsx: Removed fallback bookmark loading
   - Removed fetchBookmarks import and calls
   - Use bookmarks directly from props (centralized source)

Benefits:
- Single source of truth for bookmarks
- No duplicate fetching across components
- Streaming + auto-decrypt for better UX
- Simpler, more maintainable code
- DRY principle: one place for bookmark loading
2025-10-17 22:06:33 +02:00
Gigi
d1ffc8c3f9 feat: auto-decrypt bookmarks as they arrive
Simplified bookmark loading by chaining loading and decryption:
- Events with encrypted content are automatically decrypted as they arrive
- Removed separate "Decrypt" button - now automatic
- Removed individual decrypt buttons - happens automatically
- Removed handleDecryptSingleEvent and related state
- Cleaner UI with just "Load Bookmarks" and "Clear" buttons

Benefits:
- Simpler, more intuitive UX
- DRY - single flow instead of 2-step process
- Shows decryption results inline as events stream in
- Uses same collectBookmarksFromEvents for consistency

Each event with encrypted content (NIP-04 or NIP-44) is decrypted
immediately in the onEvent callback, with results displayed inline.
2025-10-17 21:44:55 +02:00
Gigi
5a5cd14df5 docs: add Amethyst-style bookmarks section to Amber.md
Documented kind:30001 bookmark format used by Amethyst:
- Public bookmarks in event tags
- Private bookmarks in encrypted content (NIP-04 or NIP-44)

Explained why explicit NIP-04 detection (?iv= check) is required:
- Helpers.hasHiddenContent() only detects NIP-44
- Without NIP-04 detection, private bookmarks never get decrypted

Added example event structure and implementation notes for both
display logic and decryption logic.
2025-10-17 21:41:25 +02:00
Gigi
2fb25da9d6 fix: detect and decrypt NIP-04 encrypted bookmark content
Added explicit NIP-04 detection in bookmarkProcessing.ts:
- Check for ?iv= in content (NIP-04 format)
- Previously only checked Helpers.hasHiddenContent() (NIP-44 only)
- Now decrypts both NIP-04 and NIP-44 encrypted bookmarks

This fixes individual bookmark decryption returning 0 private items
despite having encrypted content.
2025-10-17 21:39:15 +02:00
Gigi
21228cd212 refactor: unify debug logging under [bunker] prefix
Changed all debug console logs to use [bunker] prefix with emojis:
- 🔵 Individual decrypt clicked
- 🔓 Decrypting event (with details)
-  Event decrypted (with results)
- ⚠️  Warnings (no account, 0 private items)
-  Errors

Now users can filter console by 'bunker' to see all relevant logs.
2025-10-17 21:38:23 +02:00
Gigi
e0b86a84ba debug: add detailed logging for individual bookmark decryption
Added extensive debug logging to help diagnose decryption issues:
- Event details (kind, content length, encryption type)
- Signer information (type, availability)
- Warning when 0 private items found despite encrypted content

This will help identify why decryption might be failing silently.
2025-10-17 21:34:47 +02:00
Gigi
c3a4e41968 fix: detect NIP-04 encrypted content in bookmark events
Added explicit detection for NIP-04 encrypted content format:
- NIP-04: base64 content with ?iv= suffix
- NIP-44: detected by Helpers.hasHiddenContent()
- Encrypted tags: detected by Helpers.hasHiddenTags()

Created hasEncryptedContent() helper that checks all three cases.
Now properly shows padlock emoji and decrypt button for events with
NIP-04 encrypted content (like the example with ?iv=5KzDXv09...).
2025-10-17 21:31:21 +02:00
Gigi
f3205843ac fix: use consistent encrypted content detection for padlock and decrypt button
Fixed mismatch between padlock display and decrypt button visibility:
- Both now use Helpers.hasHiddenContent() and Helpers.hasHiddenTags()
- Previously padlock showed for ANY content, button only for encrypted
- Now both correctly detect actual encrypted content

This ensures decrypt buttons appear whenever padlocks are shown.
2025-10-17 21:27:02 +02:00
Gigi
9a03dd312f feat: add individual decrypt buttons for bookmark events
Added per-event decryption on debug page:
- Small 'decrypt' button appears on events with encrypted content
- Shows spinner while decrypting individual event
- Displays decryption results (public/private counts) inline
- Button disappears after successful decryption

Uses Helpers.hasHiddenContent() and Helpers.hasHiddenTags() to detect
which events need decryption.

Allows testing individual event decryption without batch operation.
2025-10-17 21:25:28 +02:00
Gigi
b711b21048 feat: show correct connection type on debug page
Updated debug page to display the actual account type:
- Browser Extension (type: 'extension')
- Bunker Connection (type: 'nostr-connect')
- Account Connection (fallback)

Changes:
- Section title now reflects active account type
- Connection status message updated accordingly
- No longer always shows 'Bunker Connection' regardless of type

Makes it clear to users which authentication method they're using.
2025-10-17 21:21:39 +02:00
Gigi
8eaba04d91 refactor: disable account queue globally
Set accounts.disableQueue = true on AccountManager during initialization:
- Applies to all accounts automatically
- No need for temporary queue toggling in individual operations
- Makes all bunker requests instant (no internal queueing)

Removed temporary queue disabling from bookmarkProcessing.ts since
it's now globally disabled.

Updated Amber.md to document the global approach.

This eliminates the root cause of decrypt hangs - requests no longer
wait in an internal queue for previous requests to complete.
2025-10-17 21:19:21 +02:00
Gigi
0785b034e4 perf: use shorter timeouts for debug page bookmark loading
Reduced timeouts to trust EOSE from fast relays:
- Local: 800ms (down from 1200ms)
- Remote: 2000ms (down from 6000ms)

The query completes when relays send EOSE, not when timeout expires.
Fast relays send EOSE in <1 second, so total time should be much less
than the previous 6-second wait.

Result: Debug page bookmark loading now completes in ~1-2 seconds instead of always 6 seconds.
2025-10-17 21:06:05 +02:00
Gigi
47e698f197 feat: stream bookmark events on debug page
Implemented live streaming of bookmark events as they arrive from relays:
- Events appear immediately as relays respond
- Live deduplication of replaceable events (30003, 30001, 10003)
- Keep newest version when duplicates found
- Web bookmarks (39701) not deduplicated (each unique)

Benefits:
- Much more responsive UI - see events immediately
- Better user experience with progress visibility
- Deduplication happens in real-time

Uses queryEvents onEvent callback to process events as they stream in.
2025-10-17 21:01:10 +02:00
Gigi
3a752a761a refactor: remove artificial timeouts from bookmark decryption
Removed all withTimeout wrappers - now matches debug page behavior:
- Direct decrypt calls with no artificial timeouts
- Let operations fail naturally and quickly
- Bunker responds instantly (success or rejection)

No timeouts needed because:
1. Account queue is disabled (requests sent immediately)
2. Only decrypting truly encrypted content (no wasted attempts)
3. Bunker either succeeds quickly or fails quickly

This makes bookmark decryption instant, just like the debug page
encryption/decryption tests.
2025-10-17 20:54:45 +02:00
Gigi
f6cc49c07a fix: only decrypt events with actual encrypted content
Use applesauce Helpers.hasHiddenContent() instead of checking for
any content. This properly detects encrypted content and avoids
sending unnecessary decrypt requests to Amber for events that just
have plain text content.

Before: (evt.content && evt.content.length > 0)
After: Helpers.hasHiddenContent(evt)

Result:
- Only events with encrypted content sent to Amber
- Reduces unnecessary decrypt requests
- Faster bookmark loading
2025-10-17 20:53:25 +02:00
Gigi
5c4fca9cc9 docs: document critical queue disabling requirement in Amber.md
Added findings about applesauce-accounts queue issue:
- Queue MUST be disabled for batch decrypt operations
- Default queueing blocks all requests until first completes
- This was the primary cause of hangs and timeouts
- Updated performance improvements section with all optimizations
- Updated conclusion with key learnings

Ref: https://hzrd149.github.io/applesauce/typedoc/classes/applesauce-accounts.BaseAccount.html#disablequeue
2025-10-17 20:47:13 +02:00
Gigi
536a7ce1fa fix: disable account queue during batch decrypt operations
The applesauce BaseAccount queues requests by default, waiting for
each to complete before sending the next. This caused decrypt requests
to timeout before ever reaching Amber/bunker.

Solution:
- Set disableQueue=true before batch operations
- All decrypt requests sent immediately
- Restore original queue state after completion

This should fix the hanging/timeout issue where Amber never saw
the decrypt requests because they were stuck in the account's queue.

Ref: https://hzrd149.github.io/applesauce/typedoc/classes/applesauce-accounts.BaseAccount.html#disablequeue
2025-10-17 20:46:00 +02:00
Gigi
61072aef40 refactor: remove concurrent decryption in favor of sequential
Removed mapWithConcurrency hack:
- Simpler code with plain for loop
- More predictable behavior
- Better for bunker signers (network round-trips)
- Each decrypt happens in order, no race conditions

Sequential processing is cleaner and works better with remote signers.
2025-10-17 20:44:48 +02:00
Gigi
b7ec1fcf06 fix: add 5s timeout and smart encryption detection for bookmarks
Changes:
- Use 5-second timeout instead of 30 seconds
- Detect encryption method from content format
- NIP-04 has '?iv=' in content, NIP-44 doesn't
- Try the likely method first to avoid unnecessary timeouts
- Falls back to other method if first fails

This should:
- Prevent hanging forever (5s timeout)
- Be much faster than 30s when wrong method is tried
- Usually decrypt instantly when right method is used first
2025-10-17 20:42:46 +02:00
Gigi
d2fd8fb8fe perf: remove 30s timeout from bookmark decryption
The withTimeout wrapper was causing decrypt operations to wait
30 seconds before failing, even when bunker rejection was instant.

Now uses the same direct approach as the debug page encryption tests:
- No artificial timeouts
- Fast natural failures
- Should reduce decrypt time from 30s+ to near-instant

Fixes slow bookmark loading when bunker doesn't support nip44.
2025-10-17 20:40:07 +02:00
Gigi
68ee1b3122 feat: add clear button for bookmark data
- Right-aligned clear button in the same row as load/decrypt
- Clears events, stats, and timing data
- Disabled when no data to clear
2025-10-17 20:35:17 +02:00
Gigi
a37735fc1c refactor: show decrypted bookmarks above loaded events 2025-10-17 20:33:25 +02:00
Gigi
de0f587174 refactor: display bookmark event stats on separate lines 2025-10-17 20:21:24 +02:00
Gigi
f977561779 feat: restore padlock emoji for encrypted content indicator 2025-10-17 20:21:07 +02:00
Gigi
043ea168fb feat: display full event ID for easy copy/paste 2025-10-17 20:20:48 +02:00
Gigi
5336bafed4 refactor: remove emojis from bookmark event display 2025-10-17 20:20:24 +02:00
Gigi
c51291bf81 feat: add performance timing to bookmark loading and decryption
- Track load and decrypt operation durations
- Display live timing with spinner during operations
- Show completed timing in milliseconds
- Uses same Stat component as encryption timing
2025-10-17 20:19:48 +02:00
Gigi
489e48fe4d feat: enhance bookmark event display with detailed info
- Show kind names (Simple List, Replaceable List, etc)
- Display data size in human-readable format (B, KB, MB)
- Show count of public bookmarks per event
- Indicate presence of encrypted content
- Show d-tag and title for better identification
2025-10-17 20:17:58 +02:00
Gigi
744a145e9f fix: resolve linting errors in App.tsx and async.ts 2025-10-17 20:09:20 +02:00
Gigi
7ad925dbd3 feat: add bookmark loading and decryption section to debug page 2025-10-17 20:08:08 +02:00
Gigi
a69298a3a9 perf(bunker): non-blocking bookmark decryption with concurrency limit
- Add withTimeout and mapWithConcurrency helpers in utils/async.ts
- Refactor collectBookmarksFromEvents to decrypt with 6-way concurrency
- Public bookmarks collected immediately, private decrypted in parallel
- Each decrypt wrapped with 30s timeout safety net
- Document non-blocking publish and concurrent decrypt in Amber.md
2025-10-17 13:27:50 +02:00
Gigi
2c3aff0407 perf(bunker): make NIP-46 publish non-blocking at app wiring level; resolve immediately and let responses drive timing/results 2025-10-17 13:09:42 +02:00
Gigi
aad35d41db fix(debug): return benign object from fire-and-forget publish so timing UI remains stable 2025-10-17 13:06:17 +02:00
Gigi
cc6189a5d9 perf(bunker): fire-and-forget NIP-46 publish in app wrapper so UI isn’t blocked waiting on relay publish; encryption/decryption results now display immediately on /debug 2025-10-17 13:04:59 +02:00
Gigi
18bf8f9a2c ui(debug): use existing color pattern for red disconnect button with proper styling and hover effects 2025-10-17 12:56:47 +02:00
Gigi
37f3a32a1c fix(debug): use inline red styling for disconnect button since btn-danger class doesn't exist 2025-10-17 12:56:06 +02:00
Gigi
c9678564a5 ui(debug): change disconnect button to red (btn-danger) for better visual indication 2025-10-17 12:54:26 +02:00
Gigi
721c18c509 ui(debug): add Reset button to restore default payload text 2025-10-17 12:53:44 +02:00
Gigi
9e30fe683b ui(debug): left-align encrypt and decrypt buttons in both NIP-44 and NIP-04 sections 2025-10-17 12:53:20 +02:00
Gigi
7fff50c146 ui(debug): move Encrypt/Decrypt buttons above Encrypted text in both NIP-44 and NIP-04 sections 2025-10-17 12:52:40 +02:00
Gigi
fc1c845b67 ui(debug): change 'cipher' labels to 'Encrypted:' for better clarity 2025-10-17 12:52:12 +02:00
Gigi
c2ec1f3677 ui(debug): move Clear logs button below Show all checkbox 2025-10-17 12:51:37 +02:00
Gigi
0cbd357856 ui(debug): right-align all buttons using justify-end 2025-10-17 12:51:21 +02:00
Gigi
26ea9ed547 fix(lint): remove unused global variable declarations from Debug component 2025-10-17 12:50:49 +02:00
Gigi
9cbbecb32c ui(debug): increase debug logs height from max-h-96 to max-h-192 (2x taller) 2025-10-17 12:49:59 +02:00
Gigi
db12c89731 ui(debug): add character-wrap (break-all) to ciphertext textboxes 2025-10-17 12:49:28 +02:00
Gigi
6f413deb90 ui(debug): increase ciphertext textarea height to 5 lines (h-20) 2025-10-17 12:48:57 +02:00
Gigi
0127e2dc86 ui(debug): change page title from 'Bunker Debug' to 'Debug' 2025-10-17 12:48:25 +02:00
Gigi
7743928702 ui(debug): increase log area height from max-h-64 to max-h-96 (3x taller) 2025-10-17 12:48:01 +02:00
Gigi
bf76150fc1 ui(debug): show spinner in place of millisecond number during measurement 2025-10-17 12:47:36 +02:00
Gigi
c62107172b ui(debug): make ciphertext and plaintext fields multiline with proper whitespace handling 2025-10-17 12:47:13 +02:00
Gigi
a253587dfa ui(debug): add subtle background to payload textarea for better editability indication 2025-10-17 12:46:57 +02:00
Gigi
1938533d53 ui(debug): replace animated timing with spinner during measurement 2025-10-17 12:46:43 +02:00
Gigi
28943c55bd style(debug): update ciphertext and plaintext display to match logs textbox style 2025-10-17 12:46:21 +02:00
Gigi
791bbb68b6 fix(debug): implement proper stopwatch timing that counts up from 0ms in real-time 2025-10-17 12:44:29 +02:00
Gigi
ec8adcc794 refactor(debug): move plaintext display below buttons for better visual flow 2025-10-17 12:43:06 +02:00
Gigi
68058e7661 refactor(debug): move encrypt buttons next to decrypt buttons for better UX 2025-10-17 12:42:22 +02:00
Gigi
416c62369c refactor: extract VersionFooter component to eliminate duplication between debug and settings 2025-10-17 12:41:39 +02:00
Gigi
a19dd53423 feat(debug): add live performance timing with digital stopwatch display 2025-10-17 12:40:22 +02:00
Gigi
79ec33b79a style(debug): format NIP specifications as NIP-44 and NIP-04 2025-10-17 12:37:59 +02:00
Gigi
be881b957c feat(debug): update log description to 'Recent bunker logs:' 2025-10-17 12:36:50 +02:00
Gigi
244872e9f2 style(debug): move debug logs controls below the log output 2025-10-17 12:36:36 +02:00
Gigi
1397f7f0f4 style(debug): apply settings page styling structure and layout 2025-10-17 12:36:10 +02:00
Gigi
96424dd65c fix: resolve all linting issues - replace empty catch blocks and fix explicit any types 2025-10-17 12:33:53 +02:00
Gigi
9efc5459fb feat(debug): replace debug logs button with proper HTML checkbox element 2025-10-17 12:32:53 +02:00
Gigi
7e02168e54 feat(debug): make debug logs button show toggleable checkmark (✓/☐) 2025-10-17 12:32:29 +02:00
Gigi
f8e6b3e828 refactor(debug): move time measurements to dedicated Performance Timing section 2025-10-17 12:32:12 +02:00
Gigi
c06176bfc9 feat(debug): add bunker login section as first section of debug page 2025-10-17 12:31:31 +02:00
Gigi
e2a1701000 refactor(debug): move debug logs section to end with improved layout 2025-10-17 12:30:14 +02:00
Gigi
d7703ceef4 style(debug): use regular HTML checkmark instead of FontAwesome icon 2025-10-17 12:29:09 +02:00
Gigi
93daabc673 style(debug): improve cipher text wrapping with overflowWrap anywhere 2025-10-17 12:28:43 +02:00
Gigi
9264245944 style(debug): make Clear logs button a proper secondary button 2025-10-17 12:28:14 +02:00
Gigi
f56423040b feat(debug): add checkmark icon to debug logs button when enabled 2025-10-17 12:28:04 +02:00
Gigi
4b91504a50 feat(debug): clarify button text to 'Show all applesauce debug logs' 2025-10-17 12:27:45 +02:00
Gigi
1f0f7fef5e feat(debug): update title to 'Bunker Debug' for clarity 2025-10-17 12:27:25 +02:00
Gigi
6aced653fb feat(debug): add clock icon to time measurements for better visual clarity 2025-10-17 12:27:14 +02:00
Gigi
0899482869 style(debug): make Encrypt (nip04) and Clear buttons proper secondary buttons 2025-10-17 12:26:51 +02:00
Gigi
1bdfa1e6e1 style(debug): apply same max-width as reading view to debug page 2025-10-17 12:26:31 +02:00
Gigi
f22a8f15c0 style(debug): improve debug page styling and layout consistency 2025-10-17 12:22:31 +02:00
Gigi
bf6394fc7d feat(debug): add version and git commit footer to /debug page 2025-10-17 12:20:43 +02:00
Gigi
6f08586e8f feat(debug): improve layout/readability with sections, code boxes, and stats badges 2025-10-17 12:19:09 +02:00
Gigi
d60a4a24ad feat(debug): show encrypt/decrypt durations for nip04/nip44 on /debug page 2025-10-17 12:14:59 +02:00
Gigi
51069f3623 feat(debug): add debug toggle and clear logs; disable account queueing for nostr-connect 2025-10-17 12:12:25 +02:00
Gigi
1407af22e3 feat(debug): interactive /debug page (manual nip04/nip44 encrypt/decrypt, live logs); add DebugBus and wire signer logs 2025-10-17 10:50:20 +02:00
Gigi
ea6220277d feat(debug): add /debug page with NIP-46 encrypt→decrypt probes for nip04/nip44 2025-10-17 10:37:45 +02:00
Gigi
fbffa03dad docs(amber): summarize bunker decrypt investigation, evidence, and next steps 2025-10-17 09:48:11 +02:00
Gigi
a74760d804 chore(bunker): increase decrypt timeouts (probe 10s, bookmark decrypt 30s) 2025-10-17 09:36:13 +02:00
Gigi
c4b0a712d2 chore(bunker): log NIP-46 method from event content to debug decrypt calls 2025-10-17 09:34:31 +02:00
Gigi
1fecf9c7f4 fix(bunker): accept remote===pubkey for Amber; remove invalid-state warning 2025-10-17 01:26:32 +02:00
Gigi
7be21203d9 chore(types): cast through unknown for protected publish/subscription access in debug wrappers 2025-10-17 01:25:21 +02:00
Gigi
f65f2c6597 chore(lint): remove explicit any types, add deps for useEffect, and type relay logging 2025-10-17 01:24:41 +02:00
Gigi
227def4328 chore(lint): replace empty catch blocks with warnings; keep strict rules 2025-10-17 01:22:53 +02:00
Gigi
b506624f57 fix(bunker): use encrypt→decrypt roundtrip for nip44/nip04 probe to avoid false timeouts 2025-10-17 01:19:37 +02:00
Gigi
fbb6a0a153 fix(bunker): merge signer.relays with app RELAYS to include local Amber relays 2025-10-17 01:13:03 +02:00
Gigi
528de32689 fix(bunker): wire NostrConnectSigner to RelayPool publish/subscription statics for NIP-46 responses 2025-10-17 01:07:35 +02:00
Gigi
230e5380ca chore(bunker): expand debug logs for NIP-46 publish/subscribe (tags, content length) 2025-10-17 01:05:13 +02:00
Gigi
349237d097 fix(bunker): preserve signer context when wrapping publish/subscription for decrypt responses 2025-10-17 01:01:44 +02:00
Gigi
d4df9f0424 chore: commit pending changes to App and LoginOptions 2025-10-17 00:55:47 +02:00
Gigi
2f68e84002 debug(bunker): log NIP-46 request body preview (method, params, content slice)
- Helps align our request shape with Amber's expected BunkerRequest format
2025-10-17 00:53:58 +02:00
Gigi
b18dcc29cd revert: do not block when remote === user pubkey
- Amber may legally use user pubkey as remote id
- Remove validation and warning that caused false negatives
2025-10-17 00:45:39 +02:00
Gigi
680169e312 fix(bunker): validate bunker URI - remote must differ from user pubkey
- Prevents invalid state where Amber remote equals user pubkey
- Show actionable error to generate fresh connect link in Amber
2025-10-17 00:42:14 +02:00
Gigi
11753c4515 debug(bunker): add post-connect decrypt probe (nip04/nip44) with timeout
- Verifies Amber responds to NIP-46 decrypt after connect
- Logs probe results under [bunker]; non-blocking to UX
2025-10-17 00:29:52 +02:00
Gigi
bd29dfd65f chore(bunker): warn if remote pubkey equals user pubkey (invalid state)
- Add sanity check and toast guidance to reconnect via Amber
- Helps catch misconfigured bunker URIs that would never respond to requests
2025-10-17 00:26:54 +02:00
Gigi
4b1ae838e5 chore: add Amber to .gitignore 2025-10-17 00:23:58 +02:00
Gigi
85599d3103 fix(bunker): guarded connect with explicit permissions on restore
- Pass getDefaultBunkerPermissions() to connect() to ensure decrypt perms
- Keeps existing reconnection safeguards and logging
- Aims to make Amber accept decrypt requests after restore
2025-10-17 00:21:46 +02:00
Gigi
4603c5a258 fix(bunker): guarded connect after subscription to enable decrypt
- After opening subscription, call connect() once per session if remote is present
- Helps Amber authorize decrypt ops; safe-guarded and logged
- Keep isConnected=true for subsequent requireConnection() paths
2025-10-17 00:19:21 +02:00
Gigi
ec45fbc5e8 debug(bunker): log signer publish/subscribe calls and relay connectivity
- Wrap NostrConnectSigner publish/subscription to log relays and filters
- Log relayPool connectivity snapshot before bookmark decryption
- Helps diagnose decrypt requests not reaching Amber
2025-10-17 00:17:00 +02:00
Gigi
53400334b2 Revert "fix: skip bookmark decryption for bunker signers"
This reverts commit af4ff7081a.
2025-10-17 00:12:20 +02:00
Gigi
af4ff7081a fix: skip bookmark decryption for bunker signers
- Bunker (NIP-46) signers don't reliably support async decrypt operations
- Skip attempting to decrypt private bookmarks when using bunker
- Users can still see all public bookmarks
- Use extension signer for access to encrypted private bookmarks
- Prevents 15+ second hangs waiting for decrypt responses that won't come
2025-10-17 00:11:20 +02:00
Gigi
7f21b8ed76 fix: add startup delay to allow bunker subscription to fully establish
- Small 100ms delay after opening signer subscription
- Ensures the subscription is ready to receive decrypt responses
- May fix timeout issues with bunker decrypt operations
2025-10-17 00:09:27 +02:00
Gigi
55e44dcc9c debug: increase decrypt timeout to 15 seconds
- Give bunker operations more time to respond
- Will help determine if this is a timing issue or a fundamental limitation
- Still logging timeout errors for visibility
2025-10-17 00:05:53 +02:00
Gigi
59dac947ab fix: actually reorder bunker relay addition before signer recreation
- Previous commit had wrong message, code wasn't actually changed
- Now properly add relays to pool before creating NostrConnectSigner
- Ensures publishMethod/subscriptionMethod have full relay list available
2025-10-17 00:00:57 +02:00
Gigi
7d33c3c024 fix: add bunker relays to pool BEFORE recreating signer
- Bunker relays must be in pool when signer sets up publishMethod/subscriptionMethod
- Previously added after signer recreation, leaving pool incomplete
- This should fix decrypt operations that rely on publishMethod being set up correctly
- Same fix pattern as we used for signing
2025-10-16 23:59:14 +02:00
Gigi
38a014ef84 debug: verify subscriptionMethod and publishMethod on recreated signer
- Check if recreated NostrConnectSigner has methods needed for decrypt operations
- This will help identify if the issue is missing publishMethod for sending decrypt requests
- Or missing subscriptionMethod for receiving responses
2025-10-16 23:57:32 +02:00
Gigi
f451348430 debug: add logging to bookmark decrypt error handling
- Log nip04/nip44 decrypt errors instead of silently ignoring
- Will help identify why bookmark decryption is timing out with bunker
- Timeout errors will now be visible in console
2025-10-16 23:55:30 +02:00
Gigi
685aaf43b0 fix: add timeout to bookmark decryption to prevent hanging
- Wrap nip04/nip44 decrypt calls with 5 second timeout
- Prevents UI from hanging if decrypt request doesn't receive response
- Allows graceful degradation instead of infinite wait
- With bunker, decrypt responses may not arrive if perms/relay issues
2025-10-16 23:54:31 +02:00
Gigi
d6a20b5272 debug: add [bunker] prefix to bookmark decryption logging
- Better filtering of bunker-related logs
- Track when signer candidate is being selected
2025-10-16 23:50:16 +02:00
Gigi
d8d7a19fa1 fix: pass account.signer to EventFactory instead of full account
- EventFactory expects an EventSigner interface with signEvent method
- account.signer is the actual NostrConnectSigner instance
- Add debug logging to trace signer type
- This should fix signing hanging when using bunker
2025-10-16 23:46:25 +02:00
Gigi
63626fae3a fix: recreate NostrConnectSigner with pool on account restore
- Restored signers from JSON don't have pool context
- Recreate signer with pool passed explicitly to fix subscriptionMethod binding
- This ensures signing requests are properly sent/received through the pool
- Fixes hanging on signing after page reload
2025-10-16 23:44:43 +02:00
Gigi
de09ef2935 fix: avoid adding duplicate bunker relays to pool
- Only add bunker relays that aren't already in the pool
- Prevents duplicate subscriptions that could cause signing hangs
- Improves stability when account is reconnected
2025-10-16 23:43:03 +02:00
Gigi
bcb28a63a7 refactor: cleanup after bunker signing implementation
- Remove reconnectBunkerSigner function, inline logic into App.tsx for better control
- Clean up try-catch wrapper in highlightCreationService, signing now works reliably
- Remove extra logging from signing process (already has [bunker] prefix logs)
- Simplify nostrConnect.ts to just export permissions helper
- Update api/article-og.ts to use local relay config instead of import
- All bunker signing tests now passing 
2025-10-16 23:39:31 +02:00
Gigi
a479903ce3 debug: log signer state before signing 2025-10-16 23:34:59 +02:00
Gigi
567d105261 fix: restore isConnected = true so signing doesn't hang
- Without this, requireConnection() tries to connect() again
- That breaks the entire signing flow
- Mark signer as connected after opening subscription
2025-10-16 23:33:31 +02:00
Gigi
83743c5a9f fix: remove decrypt queue that was blocking highlight signing
- The global decrypt queue in bookmarkProcessing was getting stuck
- Caused all NIP-46 operations to hang indefinitely
- Decrypt already has per-call timeouts; queue was unnecessary
- Highlights should now sign immediately without waiting for bookmarks
2025-10-16 23:30:18 +02:00
Gigi
0b8f88ea1d revert(highlight): avoid pre-connect; rely on requireConnection during sign
- Remove manual connect/open in highlight flow
- Prevent side-effects that may interfere with pending requests
2025-10-16 23:28:06 +02:00
Gigi
fadc755930 fix(highlight): ensure NIP-46 signer is open/connected before signing
- Pre-open subscription and connect() if bunker signer present
- Restores reliable highlight signing with Amber (NIP-46)
2025-10-16 23:26:28 +02:00
Gigi
f67f171e64 fix(bookmarks): serialize decrypt/unlock NIP-46 operations
- Queue decrypt/unlock to avoid overlapping requests hanging the provider
- Keep timeouts and detailed [bunker] logs
- Should stop decrypt flood from blocking highlight signing
2025-10-16 23:21:52 +02:00
Gigi
449c59015e refactor(api): import RELAYS from central config to keep DRY
- Remove duplicated relay array from api/article-og.ts
- Import from src/config/relays.ts instead
2025-10-16 23:20:57 +02:00
Gigi
4d697e6a79 chore(relays): update RELAYS list (include relay.nsec.app early)
- Aligns app relay set with commonly used relays
- May improve connectivity and latency for NIP-46 roundtrips
2025-10-16 23:20:05 +02:00
Gigi
04ae70873a fix: restore direct pool bindings for NIP-46 methods
- Revert logging wrappers around subscription/publish
- Use pool.subscription.bind(pool) and pool.publish.bind(pool)
- Avoid any side effects interfering with signer requests
2025-10-16 23:18:37 +02:00
Gigi
2f8a64826a debug: restore [bunker] logs around highlight signing
- Log before/after factory.sign for highlights
- Surface errors to console for fast diagnosis
2025-10-16 23:16:59 +02:00
Gigi
11cb3542ee fix: revert forced connect on reconnection to restore signing
- Remove connect(undefined, permissions) on restore
- Let requireConnection() trigger connect per op
- Keeps highlights signing working as before while we debug decrypt
2025-10-16 23:11:08 +02:00
Gigi
905296621c fix: pass permissions on reconnect to ensure decrypt allowed
- Call signer.connect(undefined, permissions) when restoring account
- Ensures bunker re-grants decrypt (nip04/nip44) if needed
- Keeps implementation aligned with applesauce examples
2025-10-16 23:06:06 +02:00
Gigi
769484bc0d debug: log NIP-46 subscribe/publish traffic
- Wrap subscriptionMethod/publishMethod to log relays, filters, responses
- Helps confirm decrypt/sign requests are actually sent and on which relays
- Continue using applesauce-recommended binding pattern
2025-10-16 22:58:41 +02:00
Gigi
27ff4cef22 fix: properly connect NostrConnectSigner on reconnection
- Call signer.connect() instead of forcing isConnected
- Add [bunker] logs for connect lifecycle
- Should unblock nip44/nip04 decrypt calls that were timing out
2025-10-16 22:55:17 +02:00
Gigi
a352e2616e fix: prevent decrypt hangs with timeout + fallback
- Wrap nip44/nip04 decrypt and unlockHiddenTags in timeouts
- Fallback nip44->nip04 if nip44 hangs/fails
- Add detailed [bunker] logs for each stage
- Keeps UI responsive while debugging bunker responses
2025-10-16 22:51:58 +02:00
Gigi
77cbb9394f refactor: simplify bunker implementation following applesauce patterns
- Remove bunkerFixVersion migration logic
- Simplify account loading to match applesauce examples
- Simplify reconnectBunkerSigner (no waiting, no complex logging)
- Direct nip04/nip44 exposure from signer (like ExtensionAccount)
- Clean up bookmark service account checking
- Keep debug logs for now until verified working
2025-10-16 22:48:46 +02:00
Gigi
39c8b3dfe4 fix: auto-clear old bunker accounts that were created with wrong setup
- Old bunker accounts were created before proper method binding
- Add version check to clear nostr-connect accounts once
- Preserves extension accounts
- Users will need to reconnect bunker (one-time migration)
2025-10-16 22:45:56 +02:00
Gigi
7bd11e695e fix: use proper NostrConnectSigner setup per applesauce examples
- Was setting NostrConnectSigner.pool (wrong approach)
- Should set subscriptionMethod and publishMethod directly
- Follows the pattern from applesauce/packages/examples/src/examples/signers/bunker.tsx
- This is the correct way to wire up the signer with the relay pool
2025-10-16 22:44:56 +02:00
Gigi
a76b703d36 fix: cache wrapped nip04/nip44 objects instead of using getters
- Getters were returning new objects each time
- Code was getting reference then calling decrypt on it
- Now assign wrapped objects directly as properties
- This ensures our logging wrappers are actually used
2025-10-16 22:42:47 +02:00
Gigi
df51173405 debug: wrap nip04/nip44 methods with [bunker] logging
- Log when decrypt/encrypt methods are called
- Log when they complete or fail
- Show pubkey and ciphertext/plaintext lengths
- This will tell us if decrypt is hanging in the signer or never returning
2025-10-16 22:41:04 +02:00
Gigi
a79d7f9eaf debug: enable NostrConnectSigner logging to diagnose decrypt hang
- Add detailed logging for signer subscription opening
- Enable debug logs for NostrConnectSigner via localStorage
- This will show if requests are being sent and responses received
- Helps diagnose why decrypt requests hang indefinitely
2025-10-16 22:40:00 +02:00
Gigi
1032a46456 fix: wait for bunker relay connections before marking signer ready
- Decryption was hanging because relay connections weren't established
- NostrConnectSigner sends requests via relays but pool wasn't connected
- Now wait for at least one bunker relay to be connected (5s timeout)
- Prevents decrypt/sign requests from being sent to unconnected relays
- Adds detailed logging for connection status
2025-10-16 22:37:45 +02:00
Gigi
ae997758ab debug: add detailed [bunker] logs for bookmark decryption
- Log account properties and nip04/nip44 availability
- Log signer fallback logic
- Log each decryption attempt (nip44 and nip04)
- Log success/failure for hidden tags and content decryption
- Helps diagnose why bunker decryption isn't working
2025-10-16 22:36:00 +02:00
Gigi
91a827324d fix: expose nip04/nip44 on NostrConnectAccount for bookmark decryption
- NostrConnectSigner has nip04/nip44 but not exposed at account level
- ExtensionAccount exposes these via getters, NostrConnectAccount didn't
- Add properties dynamically during reconnection for compatibility
- Enables private bookmark decryption with bunker accounts
2025-10-16 22:34:18 +02:00
Gigi
bf849c9faa refactor: clean up bunker implementation for better maintainability
- Extract reconnectBunkerSigner into reusable helper function
- Reduce excessive debug logging in App.tsx (90+ lines → 30 lines)
- Simplify account restoration logic with cleaner conditionals
- Remove verbose signing logs from highlightCreationService
- Keep only essential error logs for debugging
- Follows DRY principles and applesauce patterns
2025-10-16 22:32:06 +02:00
Gigi
118ab46ac0 fix: add bunker relays to relay pool for signing requests
- NostrConnectSigner uses its own relay list for signing requests
- Pool must be connected to bunker relays to send/receive requests
- Add bunker relays to pool when reconnecting after page load
- This fixes signing hanging indefinitely
2025-10-16 22:28:54 +02:00
Gigi
d2f2b689f9 fix: create and setup pool BEFORE loading accounts from localStorage
- NostrConnectAccount.fromJSON needs NostrConnectSigner.pool to be set
- Move pool creation and setup before accounts.fromJSON()
- This fixes 'Missing subscriptionMethod' error on page reload
- Now bunker accounts can be properly restored from localStorage
2025-10-16 22:25:15 +02:00
Gigi
5229e45566 fix: remove unused getDefaultBunkerPermissions import from App.tsx
- Import was no longer needed after removing connect() call
- Fixes eslint no-unused-vars error
- All linter and type checks now pass
2025-10-16 22:22:16 +02:00
Gigi
b17043e85d debug: add detailed logging for account restoration from localStorage
- Log raw accounts JSON from localStorage
- Log parsed account count and types
- Log active ID lookup and restoration steps
- This will help diagnose why accounts aren't persisting across refresh
2025-10-16 22:21:05 +02:00
Gigi
19ca909ef5 fix: setup pool and relays BEFORE bunker reconnection subscription
- Move NostrConnectSigner.pool assignment before active account subscription
- Move pool.group(RELAYS) before subscription
- This ensures pool is ready when bunker signer tries to send requests
- The subscription can fire immediately, so pool must be configured first
- Add log to confirm pool assignment
2025-10-16 22:17:48 +02:00
Gigi
f7ff309b6e fix: set isConnected=true after opening restored bunker signer
- After page reload, signer is restored with isConnected=false
- When signing, requireConnection() would call connect() again without permissions
- Now we set isConnected=true after open() to prevent re-connection
- The bunker remembers permissions from initial connection
- This ensures signing works after page refresh
2025-10-16 22:16:06 +02:00
Gigi
ea5a8486b9 fix: don't call connect() again on restored bunker signer
- fromBunkerURI() already calls connect() with permissions during login
- Calling connect() again breaks the connection state
- Just call open() to ensure subscription is active
- This matches the pattern in applesauce examples which don't reconnect
- Log final signer status including relays for debugging
2025-10-16 22:15:02 +02:00
Gigi
58897b3436 fix: prevent double reconnection and add status checks after connect
- Track reconnected accounts to avoid double-connecting
- Log signer status after open() and connect() to verify state
- This should prevent the double reconnection issue
- Will help diagnose if connection is being lost immediately
2025-10-16 22:14:12 +02:00
Gigi
6a59ecfa47 debug: prefix all bunker logs with [bunker] for easy filtering
- Update App.tsx reconnection logs
- Update highlightCreationService signing logs
- Update LoginOptions error logs
- Makes it easy to filter console with 'bunker' keyword
2025-10-16 22:12:56 +02:00
Gigi
272066c6e0 debug: add comprehensive logging for bunker reconnection and signing
- Add detailed logs for active account changes and bunker detection
- Log signer status (listening, isConnected, hasRemote)
- Log each step of reconnection process
- Add signing attempt logs in highlightCreationService
- This will help diagnose where the signing process hangs
2025-10-16 22:08:14 +02:00
Gigi
0426c9d3b0 fix: correct Accounts import in App.tsx
- Import Accounts from 'applesauce-accounts' instead of 'applesauce-accounts/accounts'
- Fixes TypeScript error TS2305
- All linter and type checks now pass
2025-10-16 21:58:08 +02:00
Gigi
c22419ba0e fix: ensure bunker signer reconnects with permissions on app restore
- Create centralized getDefaultBunkerPermissions() in nostrConnect service
- Update LoginOptions to use centralized permissions
- Add bunker reconnection logic in App.tsx on active account change
- Reconnect bunker signer with open() and connect() when restored from localStorage
- Surface permission errors to users via toast in useHighlightCreation
- Ensures highlights, reactions, settings, and bookmarks work after page reload with bunker
2025-10-16 21:56:31 +02:00
Gigi
8278fed2fb fix: request NIP-46 permissions for bunker signing
- Add explicit signing permissions for event kinds: 5, 7, 17, 9802, 30078, 39701, 0
- Add encryption/decryption permissions: nip04_encrypt/decrypt, nip44_encrypt/decrypt
- Improve error messages when bunker permissions are missing or denied
- Add debug logging hint for bunker permission issues in write service
- This ensures highlights, reactions, settings, reading positions, and web bookmarks all work with bunker
2025-10-16 21:47:59 +02:00
Gigi
b24a65b490 feat: add Login with Bunker authentication option
- Wire NostrConnectSigner to RelayPool in App.tsx
- Create LoginOptions component with Extension and Bunker login flows
- Show LoginOptions in BookmarkList when user is logged out
- Add applesauce-accounts and applesauce-signers to vite optimizeDeps
- Support NIP-46 bunker:// URI authentication alongside extension login
2025-10-16 21:17:34 +02:00
Gigi
fb509fabd8 style(settings): add proper spacing around middot separator between version and commit 2025-10-16 20:59:27 +02:00
Gigi
d21285123f feat(settings): separate version and commit links - version links to release, commit links to commit 2025-10-16 20:59:09 +02:00
Gigi
1029b6be0c feat(settings): link version to GitHub release page instead of commit 2025-10-16 20:57:57 +02:00
Gigi
3fff9455a1 docs: update CHANGELOG.md for v0.6.24 2025-10-16 20:00:22 +02:00
Gigi
8c6232e029 chore(release): bump version to 0.6.24 2025-10-16 19:59:48 +02:00
Gigi
f6c562e9be fix(types): add global declarations for build-time defines and fix eslint issues 2025-10-16 19:58:57 +02:00
Gigi
a92b14e877 docs: update CHANGELOG.md for v0.6.23 2025-10-16 19:57:11 +02:00
Gigi
b69a956247 chore(release): bump version to 0.6.23 2025-10-16 19:54:35 +02:00
Gigi
82a8dcf6eb chore(settings): link short commit hash to GitHub and remove timestamp/branch 2025-10-16 19:35:20 +02:00
Gigi
8e19e22289 feat(settings): display app version and git commit in settings footer 2025-10-16 19:32:18 +02:00
Gigi
e167b57810 fix(api): align article-og relay usage to RelayPool.request and remove open/close 2025-10-16 19:20:54 +02:00
Gigi
ba3b82e6b5 chore(app): add RouteDebug gated by ?debug=1 to log route state 2025-10-16 19:19:33 +02:00
Gigi
b5edfbb2c9 chore(api): add structured debug logs to article-og handler with ?debug=1 2025-10-16 19:17:12 +02:00
Gigi
48048f877a fix(vercel): limit /a/:naddr rewrite to bots 2025-10-16 19:16:29 +02:00
Gigi
bd1afc54c3 docs: update CHANGELOG.md for v0.6.22 2025-10-16 16:02:02 +02:00
Gigi
a2c4bed0f5 chore: bump version to 0.6.22 2025-10-16 16:01:19 +02:00
Gigi
9bad49fe5f feat(vercel): add rewrite rule for article OG endpoint
Route /a/:naddr requests to /api/article-og for dynamic social preview tags.
2025-10-16 16:00:36 +02:00
Gigi
2aa6536496 Merge pull request #17 from dergigi/social-preview
Add dynamic social preview for article deep-links
2025-10-16 15:58:52 +02:00
Gigi
bd6d8a0342 chore(api): remove debug logging from article-og endpoint 2025-10-16 15:50:00 +02:00
Gigi
dc8e86bc57 fix(api): use history.replaceState before redirecting to SPA
Set the browser history to /a/{naddr} before redirecting to /, so when the SPA loads it sees the correct URL path.
2025-10-16 15:41:22 +02:00
Gigi
32b843908e debug: add logging and debug endpoint to article-og
Add console logging for debugging and ?debug=1 query param to see request details in browser.
2025-10-16 15:34:50 +02:00
Gigi
5a71480459 fix(api): add base tag for proper asset loading
Use named parameter syntax in Vercel rewrite and add <base href="/"> tag to ensure assets load correctly from root when serving index.html through the API.
2025-10-16 15:27:13 +02:00
Gigi
17455aa47b fix(api): serve index.html to browsers with preserved URL
Instead of redirecting, serve the static index.html file directly. The Vercel rewrite preserves the /a/{naddr} URL, allowing client-side SPA routing to work correctly.
2025-10-16 15:20:10 +02:00
Gigi
4cc32c27de fix(api): detect crawlers and redirect browsers to SPA
Browsers get 302 redirect to / where the SPA handles routing client-side with the original /a/{naddr} URL preserved. Crawlers/bots get the full HTML with OG meta tags.
2025-10-16 14:43:29 +02:00
Gigi
99bfe209a5 fix(api): use meta refresh instead of SPA boot in OG endpoint
Browsers will immediately redirect to / and load the SPA client-side, while crawlers/bots ignore meta refresh and only see the OG meta tags.
2025-10-16 14:38:17 +02:00
Gigi
0a28bfbd50 fix(api): replace any type with Filter from nostr-tools 2025-10-16 14:32:35 +02:00
Gigi
ba9fb109f6 refactor(api): DRY improvements for article OG endpoint
- Extract fetchEventsFromRelays helper to eliminate duplication
- Add setCacheHeaders helper for consistent header setting
- Parallelize article and profile fetching for faster response
- Move relayPool.close() to finally block to prevent leaks
- Remove redundant cacheKey variable and sorting
2025-10-16 14:31:39 +02:00
Gigi
ec9d2fcb49 chore(meta): add social preview image to homepage OG tags 2025-10-16 14:23:44 +02:00
Gigi
f841043e03 chore(assets): add default social preview image (1200x630) 2025-10-16 14:22:04 +02:00
Gigi
94dc95e1f0 feat(api): dynamic OG HTML for /a/{naddr} using relay metadata 2025-10-16 14:21:49 +02:00
Gigi
32a5145d8f chore(vercel): route /a/* to article OG handler 2025-10-16 14:20:58 +02:00
Gigi
a856e8ca26 docs: update CHANGELOG.md for v0.6.21 2025-10-16 09:57:13 +02:00
Gigi
d54306cf92 chore: bump version to 0.6.21 2025-10-16 09:56:06 +02:00
Gigi
9fdb96b64e Merge pull request #16 from dergigi/reading-progress-filters-part-two
feat: add reading progress filters and reads/links tabs
2025-10-16 09:55:32 +02:00
Gigi
c50aa3a243 fix: resolve TypeScript errors from merge
- Remove unused readingPositions and markedAsReadIds from useBookmarksData
- Remove eventStore parameter from useBookmarksData call
- Add reads and links fields to MeCache interface
2025-10-16 09:53:20 +02:00
Gigi
adef1a922c chore: remove completed plan file 2025-10-16 09:49:43 +02:00
Gigi
99df4d6761 chore: merge master into reading-progress-filters-part-two
Resolved conflicts by keeping feature branch changes:
- Kept /me/reads and /me/links routes (not /me/archive)
- Kept ReadingProgressFilters component and readingProgressUtils
- Kept readsService, linksService, and readingDataProcessor
- Restored files that were renamed/deleted in master
2025-10-16 09:49:13 +02:00
Gigi
5f6a414953 fix: resolve all linter errors and type issues
- Remove unused state variables (readsMap, linksMap) by using only setters
- Move VALID_FILTERS constant outside component to fix exhaustive-deps warning
- Remove unused isReading variable in ReadingProgressIndicator
- Remove unused extractUrlFromBookmark function and IndividualBookmark import
- Fix type errors in linksFromBookmarks by extracting metadata from tags instead of non-existent properties
2025-10-16 09:36:17 +02:00
Gigi
ed17a68986 refactor: simplify filter icon colors to blue (except green for completed) 2025-10-16 09:33:04 +02:00
Gigi
bedf3daed1 feat: add URL routing for reading progress filters 2025-10-16 09:32:30 +02:00
Gigi
2c913cf7e8 feat: color reading progress filter icons when active 2025-10-16 09:30:16 +02:00
Gigi
aff5bff03b refactor: use neutral text color for 'started' reading progress state 2025-10-16 09:29:41 +02:00
Gigi
e90f902f0b feat: add amber color for 'started' reading progress state (0-10%) 2025-10-16 09:28:06 +02:00
Gigi
d763aa5f15 fix: merge reading progress even when timestamp is older than bookmark 2025-10-16 09:20:24 +02:00
Gigi
9d6b1f6f84 fix: call onItem callback directly for items already in reads map 2025-10-16 09:18:32 +02:00
Gigi
9eb2f35dbf debug: add console logging to trace reading progress enrichment 2025-10-16 09:13:34 +02:00
Gigi
5f33ad3ba0 fix(reads): use setState callback pattern for background enrichment
- Replace closure over tempMap with setState callback pattern
- Ensures we always work with latest state when merging progress
- Prevents stale closure issues that block state updates
- Apply same fix to both reads and links tabs
- Fixes reading progress not updating in UI
2025-10-16 09:13:19 +02:00
Gigi
3db4855532 fix(reads): use naddr format for IDs to match reading positions
- Convert bookmark coordinates to naddr format in deriveReadsFromBookmarks
- Reading positions store progress with naddr as ID
- Using naddr format enables proper merging of reading progress data
- Simplify getReadItemUrl to use item.id directly (already naddr)
- Fixes reading progress not showing in /me/reads tab
2025-10-16 09:11:21 +02:00
Gigi
3305be1da5 feat(reads): extract image, summary, and published date from bookmark tags
- Extract metadata from tags same way BookmarkItem does (DRY)
- Add image tag extraction for article images
- Add summary tag extraction for article summaries
- Add published_at tag extraction for publish dates
- Images and summaries now display in /me/reads tab
2025-10-16 09:08:57 +02:00
Gigi
fe55e87496 fix: remove unused import from readsFromBookmarks 2025-10-16 09:06:06 +02:00
Gigi
f78f1a3460 fix(reads): use bookmark.content for article titles
- IndividualBookmark doesn't have separate title/event fields
- After hydration, article titles are stored in content field
- Simplified extraction logic to just use bookmark.content
2025-10-16 09:06:00 +02:00
Gigi
e73d89739b fix(reads): extract article titles from events using applesauce helpers
- Use getArticleTitle, getArticleSummary, getArticleImage, getArticlePublished from Helpers
- Extract metadata from bookmark.event when available
- Fallback to bookmark fields if event not hydrated
- Fixes 'Untitled' articles in Reads tab
2025-10-16 09:01:51 +02:00
Gigi
7e2b4b46c9 feat(me): populate reads/links from bookmarks instantly
- Add deriveReadsFromBookmarks helper to convert 30023 bookmarks to ReadItems
- Add deriveLinksFromBookmarks helper for web bookmarks (39701) and URLs
- Update loadReadsTab to show bookmarked articles immediately, enrich in background
- Update loadLinksTab to show bookmarked links immediately, enrich in background
- Background enrichment merges reading progress only for displayed items
- Preserve existing pull-to-refresh and empty state logic
2025-10-16 08:45:31 +02:00
Gigi
fddf79e0c6 feat: add named kind constants, streaming updates, and fix reads/links tabs
- Create src/config/kinds.ts with named Nostr kind constants
- Add streaming support to fetchAllReads and fetchLinks with onItem callbacks
- Update all services to use KINDS constants instead of magic numbers
- Add mergeReadItem utility for DRY state management
- Add fallbackTitleFromUrl for external links without titles
- Relax validation to allow external items without titles
- Update Me.tsx to use streaming with Map-based state for reads/links
- Fix refresh to merge new data instead of clearing state
- Fix empty states for Reads and Links tabs (no more infinite skeletons)
- Services updated: readsService, linksService, libraryService, bookmarkService, exploreService, highlights/fetchByAuthor
2025-10-16 08:27:10 +02:00
Gigi
cf2098a723 Merge pull request #15 from dergigi/revert-14-reading-progress-filters
Revert "Add reading progress filters and split Reads/Links tabs"
2025-10-16 08:06:06 +02:00
Gigi
5568437663 Revert "Add reading progress filters and split Reads/Links tabs" 2025-10-16 08:05:20 +02:00
Gigi
7bfd7fdf6c Merge pull request #14 from dergigi/reading-progress-filters
Add reading progress filters and split Reads/Links tabs
2025-10-16 01:46:32 +02:00
Gigi
e6876d141f fix: show skeletons during initial tab load for reads/links 2025-10-16 01:43:36 +02:00
Gigi
5bb81b3c22 fix: always show skeletons for reads/links when no data
Removed empty state messages like "No articles in your reads" and
"No links yet" - now just show loading skeletons until data arrives.

This is simpler and prevents showing empty states while data is still
being fetched in the background.

Users will only see:
- Skeletons when no data (loading or truly empty)
- "No articles/links match this filter" when filtered out
- Actual content when data is available
2025-10-16 01:40:37 +02:00
Gigi
1e8e58fa05 fix: show loading skeletons correctly for reads and links tabs
The bug was that showSkeletons checked if ANY tab had data, so if you
had highlights or bookmarks, it would never show skeletons for reads/links
even while they were still loading.

Fix: Each tab now checks its own loading state (loading && tabData.length === 0)
instead of using the shared showSkeletons variable.

This makes the logic simple and clear:
1. If loading AND no data → show skeletons
2. If not loading AND no data → show empty state
3. If has data but filtered out → show no match message
4. Otherwise → show content
2025-10-16 01:39:03 +02:00
Gigi
f44e36e4bf refactor: make code more DRY by extracting shared utilities
- Create readingProgressUtils.ts with filterByReadingProgress function
- Create readingDataProcessor.ts with shared processing functions:
  - processReadingPositions
  - processMarkedAsRead
  - filterValidItems
  - sortByReadingActivity
- Refactor readsService.ts to use shared utilities
- Refactor linksService.ts to use shared utilities
- Eliminate 100+ lines of duplicated code
- Simplify Me.tsx filter logic to 2 lines

Benefits:
- Single source of truth for reading progress filtering
- Easier to maintain and modify
- Less code duplication across services
- More testable with isolated utility functions
2025-10-16 01:36:28 +02:00
Gigi
11c7564f8c feat: split Reads tab into Reads and Links
- Reads: Only Nostr-native articles (kind:30023)
- Links: Only external URLs with reading progress
- Create linksService.ts for fetching external URL links
- Update readsService to filter only Nostr articles
- Add Links tab between Reads and Writings with same filtering
- Add /me/links route
- Update meCache to include links field
- Both tabs support reading progress filters
- Lazy loading for both tabs

This provides clear separation between native Nostr content and external web links.
2025-10-16 01:33:04 +02:00
Gigi
a064376bd8 fix: filter out 'Untitled' items from Reads tab
- Exclude Nostr articles without event data (can't fetch title)
- Exclude external URLs without proper titles
- Prevents cluttering Reads with items that have no meaningful title
- Only shows items we can properly identify and display
2025-10-16 01:25:31 +02:00
Gigi
292e8e9bda fix: only show external URLs in Reads if they have reading progress
- External URLs with 0% progress are now filtered out
- External URLs only appear if readingProgress > 0 OR marked as read
- Nostr articles still show even at 0% (bookmarked articles)
- Keeps Reads tab focused on actual reading activity for external links
2025-10-16 01:24:50 +02:00
Gigi
951a3699ca fix: replace spinners with skeleton placeholders in Me tabs
- Replace spinner in highlights tab with 'No highlights yet' message
- Replace spinner in reading-list tab with 'No bookmarks yet' message
- Only show these messages when loading is complete and arrays are empty
- Remove unused faSpinner import
- Consistent with skeleton placeholder pattern used elsewhere
2025-10-16 01:21:31 +02:00
Gigi
860ec70b1c feat: implement lazy loading for Me component tabs
- Add loadedTabs state to track which tabs have been loaded
- Create tab-specific loading functions (loadHighlightsTab, loadWritingsTab, loadReadingListTab, loadReadsTab)
- Only load data for active tab on mount and tab switches
- Show cached data immediately, refresh in background when revisiting tabs
- Update pull-to-refresh to only reload the active tab
- Show loading skeletons only on first load of each tab
- Works for both /me (own profile) and /p/ (other profiles)

This reduces initial load time from 30+ seconds to 2-5 seconds by only fetching data for the active tab.
2025-10-16 01:19:06 +02:00
Gigi
2b69c72939 refactor: simplify loading state to use unified logic
- Remove separate loadingReads state
- Keep single loading state true until ALL data is loaded
- Matches existing pattern used in other tabs
- Keeps code DRY and simple
2025-10-16 01:08:56 +02:00
Gigi
b98d774cbf fix: filter out reads without timestamps
- Exclude items without readingTimestamp or markedAt from reads
- Prevents 'Just Now' items from appearing in the reads list
- Only show reads with valid activity timestamps
2025-10-16 01:06:27 +02:00
Gigi
8972571a18 fix: keep showing skeletons while reads are loading
- Add separate loadingReads state to track reads fetching
- Show skeletons during the entire reads loading period
- Set loading=false after public data (highlights/writings) completes
- Prevents showing 'No articles match this filter' while reads are being fetched
2025-10-16 01:05:42 +02:00
Gigi
ab5d5dca58 debug: add logging to reads filtering 2025-10-16 00:59:28 +02:00
Gigi
e383356af1 feat: rename Archive to Reads and expand functionality
- Create new readsService to aggregate all read content from multiple sources
- Include bookmarked articles, reading progress tracked articles, and manually marked-as-read items
- Update Me component to use new reads service
- Update routes from /me/archive to /me/reads
- Update meCache to use ReadItem[] instead of BlogPostPreview[]
- Update filter logic to use actual reading progress data
- Support both Nostr-native articles and external URLs in reads
- Fetch and display article metadata from multiple sources
- Sort by most recent reading activity
2025-10-16 00:45:16 +02:00
Gigi
165d10c49b feat: split 'To read' filter into 'Unopened' and 'Started'
- Add 'unopened' filter (no progress, 0%) - uses fa-envelope icon
- Add 'started' filter (0-10% progress) - uses fa-envelope-open icon
- Remove 'to-read' filter
- Use classic/regular variant for envelope icons
- Update filter logic in BookmarkList and Me components
- New filter ranges:
  - Unopened: 0% (never opened)
  - Started: 0-10% (opened but not read far)
  - Reading: 11-94%
  - Completed: 95-100%
2025-10-16 00:13:34 +02:00
Gigi
e0869c436b fix: adjust 'Reading' filter to 11-94% range
- Change 'reading' filter from 10-95% to 11-94%
- Creates clearer boundaries between filters:
  - To read: 0-10%
  - Reading: 11-94%
  - Completed: 95-100%
2025-10-16 00:10:20 +02:00
Gigi
95432fc276 fix: reading position filters now work correctly in bookmarks
- Match marked-as-read event IDs to bookmark coordinate IDs
- Use eventStore to lookup events and build coordinates from them
- Add both event ID and coordinate format to markedAsReadIds set
- This fixes filtering of bookmarked articles by reading progress
- Apply same fix to both Bookmarks and Explore components
2025-10-15 23:54:44 +02:00
Gigi
1982d25fa8 feat: add fancy animation to Mark as Read button
- Icon spins 360° with bounce effect (scale up during spin)
- Button background changes to vibrant green gradient (#10b981)
- Green pulsing box-shadow effect on activation
- Button scales up slightly on click for emphasis
- Holds green state for 1.5 seconds
- Smoothly fades to gray after animation
- Final state is gray button to indicate marked status
- Uses cubic-bezier easing for modern, smooth feel
- Total animation duration: 2.5 seconds
- Prevents interaction during animation
2025-10-15 23:39:14 +02:00
Gigi
2fc64b6028 feat: change 'To read' filter to show 0-10% progress
- Update 'to-read' filter range from 0-5% to 0-10%
- Update 'reading' filter to start at 10% instead of 5%
- Adjust filter comments to reflect new ranges
2025-10-15 23:37:59 +02:00
Gigi
6e8686a49d feat: treat marked-as-read articles as 100% progress
- Fetch marked-as-read articles in useBookmarksData and Explore
- Pass markedAsReadIds through component chain (Bookmarks -> ThreePaneLayout -> BookmarkList)
- Display 100% progress for marked articles in all views (Archive, Bookmarks, Explore)
- Update filter logic to treat marked articles as completed
- Marked articles show green 100% progress bar
- Marked articles only appear in 'completed' or 'all' filters
- Remove reading position tracking from Me.tsx (not needed when all are marked)
- Clean up unused imports and variables
2025-10-15 23:36:05 +02:00
Gigi
fd5ce80a06 feat: add auto-mark as read at 100% reading progress
- Add autoMarkAsReadAt100 setting (default: false)
- Add checkbox in Layout & Behavior settings
- Automatically mark article as read after 2 seconds at 100% progress
- Trigger same animation as manual mark as read button
- Move isNostrArticle computation earlier for useCallback deps
- Move handleMarkAsRead to useCallback for use in auto-mark effect
2025-10-15 23:28:50 +02:00
Gigi
ac4185e2cc feat: merge 'Completed' and 'Marked as Read' filters into one
- Remove 'marked' filter type from ReadingProgressFilterType
- Update ReadingProgressFilters component to show only 4 filters
- Keep checkmark icon for unified 'Completed' filter
- Completed filter now shows both:
  - Articles with 95%+ reading progress
  - Articles manually marked as read (no position data or 0%)
- Remove unused faBooks icon import
- Update filter logic in BookmarkList and Me components
2025-10-15 23:22:40 +02:00
Gigi
9217077283 fix: replace spinners with skeletons during refresh in archive/writings tabs
- Changed spinner to empty state message only when not loading
- During refresh, keeps showing cached content or skeletons
- Archive: shows 'No articles in your archive' only when done loading
- Writings: shows 'No articles written yet' only when done loading
- Prevents jarring transition from skeletons to spinner during refresh
2025-10-15 23:20:54 +02:00
Gigi
b7c14b5c7c fix: restore top padding to reading progress filters
- Remove padding-top: 0 override
- Now has equal spacing top and bottom (0.5rem)
2025-10-15 23:18:31 +02:00
Gigi
9b3cc41770 refactor: rename ArchiveFilters to ReadingProgressFilters
- More accurate naming: filters are based on reading progress/position
- Renamed component: ArchiveFilters -> ReadingProgressFilters
- Renamed type: ArchiveFilterType -> ReadingProgressFilterType
- Renamed variables: archiveFilter -> readingProgressFilter
- Renamed CSS class: archive-filters-wrapper -> reading-progress-filters-wrapper
- Updated all imports and references in BookmarkList and Me components
- Updated comments to reflect reading progress filtering
2025-10-15 23:17:55 +02:00
Gigi
4c4bd2214c feat: add top border to archive filters in bookmarks sidebar
- Matches the style of bookmark type filters at top
- Visually separates archive filters from bookmarks content
2025-10-15 23:14:56 +02:00
Gigi
93c31650f4 fix: remove double border between archive filters and view controls
- Add archive-filters-wrapper class
- Remove border-bottom from bookmark-filters in wrapper
- Prevents double border (bookmark-filters border-bottom + view-mode-controls border-top)
2025-10-15 23:14:20 +02:00
Gigi
7f0d99fc29 fix: remove duplicate border between archive filters and view controls
- Remove borderTop from archive filters div
- Keep only the border from view-mode-controls CSS
2025-10-15 23:12:26 +02:00
Gigi
eb6dbe1644 feat: add archive filters to bookmarks sidebar
- Add ArchiveFilters component to bookmarks sidebar
- Filter buttons shown above view-mode-controls row
- Filters: All, To Read (0-5%), Reading (5-95%), Completed (95%+), Marked
- Only shown when kind:30023 articles are present
- Filters only apply to kind:30023 articles
- Other bookmark types (videos, notes, web) remain visible
2025-10-15 23:10:31 +02:00
Gigi
474da25f77 fix: add autoScrollToPosition to useEffect dependency array
- Fixes react-hooks/exhaustive-deps warning
- Ensures effect reruns when auto-scroll setting changes
2025-10-15 23:08:21 +02:00
Gigi
02eaa1c8f8 feat: show reading progress in Explore and Bookmarks sidebar
- Add reading position loading to Explore component
- Add reading position loading to useBookmarksData hook
- Display progress bars in Explore tab blog posts
- Display progress bars in Bookmarks large preview view
- Progress shown as colored bar (green for completed, orange for in-progress)
- Only shown for kind:30023 articles with saved reading positions
- Requires syncReadingPosition setting to be enabled
2025-10-15 23:07:18 +02:00
Gigi
8800791723 feat: add auto-scroll to reading position setting
- Add autoScrollToPosition setting (default: true)
- Add checkbox in Layout & Behavior settings
- Only auto-scroll when setting is enabled
- Allows users to disable auto-scrolling while keeping sync enabled
2025-10-15 22:53:47 +02:00
Gigi
6758b9678b fix: update 'To Read' filter to show 0-5% progress articles
- Filter now shows articles with 0-5% reading progress
- Excludes manually marked as read articles (those without position data)
- Updates comment to reflect new logic
2025-10-15 22:51:40 +02:00
Gigi
63f58e010f feat: use classic/regular bookmark icon for To Read filter
- Change from solid bookmark to regular (outline) bookmark icon
- Matches classic FontAwesome bookmark style
2025-10-15 22:46:15 +02:00
Gigi
85649ae283 Merge pull request #13 from dergigi/sync-reading-position
Add reading position sync and archive enhancements
2025-10-15 22:45:13 +02:00
Gigi
d0b814e39d fix: update Archive filter icons for consistency
- Change 'All' icon to asterisk (*) to match Bookmarks filter
- Change 'Marked as Read' icon to faBooks (custom icon)
- Maintains consistent iconography across filter types
2025-10-15 22:40:52 +02:00
Gigi
f4a227e40a fix: improve reading position calculation to reach 100%
- Add 5px threshold to detect when scrolled to bottom
- Set position to exactly 1.0 (100%) when within 5px of bottom
- Remove upper limit on saving positions (now saves 100% completion)
- Always save when reaching 100% completion (important milestone)
- Don't restore position for completed articles (100%), start from top
- Better handling of edge cases in position detection
- Matches ReadingProgressIndicator calculation logic
2025-10-15 22:39:51 +02:00
Gigi
6ef0a6dd71 refactor: match ArchiveFilters styling to BookmarkFilters
- Use same CSS classes (filter-btn) as BookmarkFilters
- Show icons only, no text labels for consistency
- Add title and aria-label for accessibility
- Keep code DRY by following established pattern
2025-10-15 22:35:45 +02:00
Gigi
5502d71ac4 feat: add filter buttons to Archive tab
- Create ArchiveFilters component with 5 filter options
- All: Show all archived articles
- To Read: Articles with 0% progress (not started)
- Reading: Articles with progress between 0-95%
- Completed: Articles with 95%+ reading progress
- Marked: Manually marked as read (no position data)
- Filter logic based on reading position data
- Show empty state when no articles match filter
- Matches BookmarkFilters styling and UX pattern
2025-10-15 22:30:44 +02:00
Gigi
5e1146b015 fix: position reading progress bar as dividing line in cards
- Move progress indicator between summary and meta sections
- Replace the border-top dividing line with progress bar
- Show 3px progress bar when reading position exists
- Show 1px gray divider when no progress (maintains original look)
- Remove absolute positioning from bottom of card
- Remove border-top from meta section to avoid double lines
2025-10-15 22:26:48 +02:00
Gigi
8f89165711 debug: add comprehensive logging for reading position sync
- Add detailed console logs with emoji prefixes for easy filtering
- Log save/load operations in readingPositionService
- Log position restore in ContentPanel with requirements check
- Log Archive tab position loading with article details
- All logs prefixed with component/service name for clarity
- Log shows position percentages, identifiers, and timestamps
- Helps debug why positions may not be showing or syncing
2025-10-15 22:23:40 +02:00
Gigi
674634326f feat: add visual reading progress indicator to archive cards
- Display reading position as a horizontal progress bar at bottom of blog post cards
- Use blue (#6366f1) for progress <95%, green (#10b981) for >=95% complete
- Load reading positions for all articles in Archive tab
- Progress bar fills from left to right showing how much has been read
- Only shown when reading progress exists and is >0%
- Smooth transition animations on progress updates
2025-10-15 22:19:18 +02:00
Gigi
30eaec5770 refactor: remove redundant handleHighlightClick from Explore
- HighlightItem now handles navigation internally
- Remove duplicate navigation logic from Explore component
- Simplifies code and ensures consistent behavior across all highlight displays
2025-10-15 22:13:14 +02:00
Gigi
0ff3c864a9 feat: add click-to-open article navigation on highlights
- Click on highlights in /me/highlights or /p/:npub pages to open referenced article
- Parse eventReference to detect kind:30023 articles and navigate to /a/{naddr}
- Fall back to urlReference for external URLs, navigate to /r/{url}
- Maintain backward compatibility with existing onHighlightClick prop
- Show pointer cursor when highlight has navigable reference
2025-10-15 22:12:03 +02:00
Gigi
ab2ca1f5e7 fix: remove unused IEventStore import in ContentPanel 2025-10-15 22:09:58 +02:00
Gigi
cf2d227f61 feat: add reading position sync across devices using Nostr Kind 30078
- Create readingPositionService.ts for save/load operations
- Add syncReadingPosition setting (opt-in via Settings > Layout & Behavior)
- Enhance useReadingPosition hook with auto-save (debounced 5s) and immediate save on navigation
- Integrate position restore in ContentPanel with smooth scroll to saved position
- Support both Nostr articles (naddr) and external URLs
- Reading positions stored privately to user's relays
- Auto-save excludes first 5% and last 5% of content to avoid noise
- Position automatically restored when returning to article
2025-10-15 22:08:12 +02:00
Gigi
2c9e6cc54e docs: update CHANGELOG.md for v0.6.20 2025-10-15 21:54:02 +02:00
Gigi
8da0a06711 chore: bump version to 0.6.20 2025-10-15 21:53:06 +02:00
Gigi
be8d857223 Merge pull request #12 from dergigi/bookmark-filter-buttons
Add bookmark filter buttons by content type
2025-10-15 21:52:37 +02:00
Gigi
d50bcd700e fix(ui): make highlight button fixed to viewport 2025-10-15 21:51:24 +02:00
Gigi
820ab1d902 fix(ui): make highlight button sticky and always visible
- Wrap button in sticky positioned container with height: 0
- Button now floats and stays visible while scrolling
- Remains within reader pane boundaries on desktop
- Uses flexbox to align button to the right side
2025-10-15 21:48:41 +02:00
Gigi
f5e9e5bf61 fix(ui): position highlight button inside reader pane
- Move HighlightButton from fixed viewport positioning to absolute positioning within main pane
- Add position: relative to .pane.main for both desktop and mobile layouts
- Button now stays within the article/reader view instead of floating outside on desktop
- Maintains proper z-index and responsive behavior
2025-10-15 21:47:28 +02:00
Gigi
40b43532e8 style: use faLink icon for external articles
- Replace faArrowUpRightFromSquare with simpler faLink icon
- More concise visual representation for external article links
2025-10-15 21:40:31 +02:00
Gigi
51a3008730 feat: add separate filter for external articles with distinct icon
- Add 'external' type to differentiate external article links from nostr-native articles
- Nostr-native articles (kind:30023) use newspaper icon
- External article links use arrow-up-right icon (faArrowUpRightFromSquare)
- Add new 'External Articles' filter button
- Update classification logic and icon display accordingly
2025-10-15 21:39:10 +02:00
Gigi
e30cbc72c3 style: dramatically reduce whitespace around bookmark filters
- Remove all padding from filter buttons
- Reduce top padding from 0.75rem to 0.25rem
- Reduce bottom margin from 0.5rem to 0.25rem
- Much tighter, more compact layout
2025-10-15 21:35:44 +02:00
Gigi
6f913262f4 style: reduce whitespace around bookmark filters on /me page
- Reduce padding on bookmark filters from 1rem to 0.5rem
- Reduce top padding of tab content when filters are present
- Tighten spacing for more compact layout
2025-10-15 21:35:11 +02:00
Gigi
0f0462e6ac feat: add bookmark filters to /me page bookmarks tab
- Add filter buttons to reading-list tab in Me component
- Apply same filtering logic as main bookmarks sidebar
- Center-align filters and remove border for cleaner look
- Show empty state message when no bookmarks match filter
2025-10-15 21:24:19 +02:00
Gigi
e353f0e2d6 style: refine bookmark filter buttons
- Make buttons smaller (32px) and more compact
- Remove borders for cleaner look
- Active state uses primary color without background
- Match icon styling used on bookmark cards
2025-10-15 21:19:16 +02:00
Gigi
ee1365d3ca feat: add bookmark filter buttons by content type
- Add BookmarkFilters component with icon-based filter buttons
- Create bookmarkTypeClassifier utility for content type classification
- Filter bookmarks by article, video, note, or web types
- Apply filters across all bookmark lists (private, public, web, sets)
- Style filter buttons to match existing UI design
2025-10-15 21:17:27 +02:00
Gigi
a215d0b026 refactor: remove lock icon from individual bookmarks
- Private bookmarks are now grouped in 'Private Bookmarks' section
- No need for redundant lock icon on each individual bookmark
- Cleaner UI with less visual clutter
- Removed faUserLock import and conditional rendering from all three views
2025-10-15 20:37:40 +02:00
Gigi
b8d76c0bd8 feat: move encrypted legacy bookmarks to Private Bookmarks section
- Only non-encrypted legacy bookmarks (kind:30001) now appear in Legacy section
- Encrypted legacy bookmarks are grouped with other private bookmarks
- Improves organization by grouping by privacy level rather than source
2025-10-15 20:36:00 +02:00
Gigi
233169b082 feat: improve bookmark section labels for clarity
- Capitalize all bookmark section labels for consistency
- Change 'Old Bookmarks (Legacy)' to 'Legacy Bookmarks' for cleaner look
- Updated labels in both BookmarkList and Me components
2025-10-15 20:35:19 +02:00
Gigi
72b9a04cd2 docs: update CHANGELOG.md for v0.6.19 2025-10-15 20:01:43 +02:00
Gigi
432715efb6 chore: bump version to 0.6.19 2025-10-15 20:01:07 +02:00
Gigi
8b2b954dde fix: prevent useBookmarksData from overwriting external URL highlights
The issue was that useBookmarksData was fetching general highlights
whenever there was no naddr, which included external URL routes (/r/*).
This caused the URL-specific highlights loaded by useExternalUrlLoader
to be overwritten after a couple seconds.

Now we skip fetching general highlights when viewing external URLs,
letting useExternalUrlLoader manage those highlights instead.
2025-10-15 19:59:54 +02:00
Gigi
c2d2bd8106 fix: prevent highlights from disappearing on external URLs
- Improve error handling in fetchHighlightsForUrl to prevent silent failures
- Remove redundant setHighlights call that was overwriting streamed highlights
- Add logging to help diagnose highlight fetching issues
- Isolate rebroadcast errors so they don't break highlight display
2025-10-15 19:56:07 +02:00
Gigi
a5c3085c59 docs: update CHANGELOG.md for v0.6.18 2025-10-15 19:49:13 +02:00
Gigi
c0332f08d6 chore: bump version to 0.6.18 2025-10-15 19:48:00 +02:00
Gigi
38a1d6caec fix: always show PWA install section with disabled button states 2025-10-15 19:43:44 +02:00
Gigi
39dd607e7b style: make zap preset buttons expand to match slider width on desktop 2025-10-15 19:43:11 +02:00
Gigi
9dc0db3e06 fix: always show App & Airplane Mode section regardless of PWA status 2025-10-15 19:42:27 +02:00
Gigi
b1eb58a385 fix: display zap split share and percentage on same line 2025-10-15 19:41:26 +02:00
Gigi
f3c6404f76 refactor: simplify zap split labels and update terminology 2025-10-15 19:39:04 +02:00
Gigi
1a42a6422d fix: disable PWA install button when installation is not possible on device 2025-10-15 19:37:57 +02:00
Gigi
2e2de4ccda docs: update CHANGELOG.md for v0.6.17 2025-10-15 19:36:50 +02:00
Gigi
4325d3a519 chore: bump version to 0.6.17 2025-10-15 19:35:36 +02:00
Gigi
51115c5f68 refactor: move Default Highlight Visibility back after Paragraph Alignment 2025-10-15 19:34:03 +02:00
Gigi
2aa6fe860b refactor: merge Layout & Navigation and Startup & Behavior into Layout & Behavior section 2025-10-15 19:33:22 +02:00
Gigi
86f39eacf8 refactor: move Default Highlight Visibility after Font Size in reading settings 2025-10-15 19:32:13 +02:00
Gigi
d15daef3ea fix: properly align Font Size buttons to right using setting-control wrapper 2025-10-15 19:31:04 +02:00
Gigi
281c70cdea style: align Font Size buttons to the right to match highlight color buttons 2025-10-15 19:29:22 +02:00
Gigi
d6d6087543 refactor: move Layout & Navigation section below Zap Splits 2025-10-15 19:28:33 +02:00
Gigi
d06e38bc19 refactor: reorder settings sections - move Startup & Behavior after Zap Splits 2025-10-15 19:28:05 +02:00
Gigi
cfc8eb0bbc feat: use friend-highlight color at 50% opacity for right side of zap sliders 2025-10-15 19:26:43 +02:00
Gigi
b85f9b79c3 feat: add zaps.svg illustration to Zap Splits section with responsive layout 2025-10-15 19:26:10 +02:00
Gigi
1b0045c737 refactor: add 50% opacity to slider track highlight color 2025-10-15 19:24:27 +02:00
Gigi
3dc8d7d440 fix: improve lightning bolt icon centering and sizing on slider thumbs 2025-10-15 19:18:57 +02:00
Gigi
bf9ca48d64 feat: replace slider thumb circles with lightning bolt icons for zap splits 2025-10-15 19:17:55 +02:00
Gigi
70441f3d59 refactor: use default highlight color for zap slider 50% mark instead of primary color 2025-10-15 19:16:16 +02:00
Gigi
431f28e861 refactor: update zap split description to match offline-first paragraph style 2025-10-15 19:15:43 +02:00
Gigi
3b1fc095c4 feat: add 50% visual indicators to zap split sliders with gradient background and tick marks 2025-10-15 19:15:14 +02:00
Gigi
9a6c7a29d0 feat: restrict settings page width to 900px matching article view max-width 2025-10-15 19:13:22 +02:00
Gigi
c1d173f40e fix: move offline-first paragraph inside flex container to prevent overlap with image 2025-10-15 19:12:17 +02:00
Gigi
f03ec5df8c refactor: move 'Use local relays as cache' checkbox after local relay paragraph 2025-10-15 19:11:36 +02:00
Gigi
6c74a12636 feat: add offline-first description at the beginning of App & Airplane Mode section 2025-10-15 19:10:38 +02:00
Gigi
39797803d3 refactor: rename section title from 'PWA & Flight Mode' to 'App & Airplane Mode' 2025-10-15 19:07:54 +02:00
Gigi
c66c1e928d refactor: swap paragraph order - Note about relays first, Install Boris second 2025-10-15 19:06:55 +02:00
Gigi
f934b641bb refactor: replace IconButton with plain icon for clear cache trash button 2025-10-15 19:06:22 +02:00
Gigi
1128a11603 refactor: reorder PWA settings - checkboxes first, then paragraphs, then install button 2025-10-15 19:05:03 +02:00
Gigi
9f90718918 refactor: reduce clear cache button size from 28 to 20 2025-10-15 19:03:43 +02:00
Gigi
067a07fc00 refactor: further reduce spacing between PWA settings elements from 0.5rem to 0.25rem 2025-10-15 19:02:09 +02:00
Gigi
1811cf045e refactor: split PWA description into two paragraphs and update text 2025-10-15 19:01:34 +02:00
Gigi
270b4f429f refactor: remove 'Install Boris as a PWA' title from settings section 2025-10-15 18:59:48 +02:00
Gigi
380acbb55f feat: hide PWA SVG illustration on mobile devices 2025-10-15 18:59:24 +02:00
Gigi
c384f0b4fb refactor: reduce spacing between PWA settings elements from 1rem to 0.5rem 2025-10-15 18:58:33 +02:00
Gigi
27cf393a03 refactor: set PWA SVG width to 30% for responsive scaling 2025-10-15 18:57:35 +02:00
Gigi
8831726913 refactor: reduce PWA SVG size to 150px width 2025-10-15 18:57:02 +02:00
Gigi
2f4327874c refactor: format and clean up pwa.svg with proper indentation and Inkscape metadata 2025-10-15 18:55:23 +02:00
Gigi
483845962e refactor: combine relay info text with PWA description into single paragraph 2025-10-15 18:53:38 +02:00
Gigi
c44b1d6349 refactor: set PWA SVG height to 100% with auto width for full vertical span 2025-10-15 18:52:20 +02:00
Gigi
79f28a142d refactor: increase PWA SVG illustration size from 120px to 200px 2025-10-15 18:51:54 +02:00
Gigi
02dd537cd9 refactor: make PWA SVG illustration span full section height 2025-10-15 18:50:50 +02:00
Gigi
5af1f14a0b refactor: merge PWA and Flight Mode settings into single section 2025-10-15 18:49:25 +02:00
Gigi
664f59a9cc refactor: show PWA button state with checkmark when installed instead of hiding section 2025-10-15 18:48:03 +02:00
Gigi
7d3641aab7 refactor: simplify PWA install text to 'Install Boris as a PWA' 2025-10-15 18:45:38 +02:00
Gigi
7924df4c67 refactor: simplify PWA section title to 'App' 2025-10-15 18:45:25 +02:00
Gigi
68a8eed4af refactor: expand PWA install text to include full terminology 2025-10-15 18:45:07 +02:00
Gigi
887db84ce7 refactor: change PWA section title to 'Boris as an App' 2025-10-15 18:44:37 +02:00
Gigi
05348fbfeb feat: add pwa.svg illustration to PWA settings section 2025-10-15 18:44:18 +02:00
Gigi
38eb6716f8 refactor: move PWA settings above Relays section 2025-10-15 18:42:31 +02:00
Gigi
d7f9cd30eb feat: always show PWA install button for testing/styling purposes 2025-10-15 18:41:40 +02:00
Gigi
922d041e0e docs: update CHANGELOG.md for v0.6.16 2025-10-15 18:31:46 +02:00
Gigi
76f4588c85 chore: bump version to 0.6.16 2025-10-15 18:30:36 +02:00
Gigi
e163b92a7e fix: remove unused handleCancelDelete function
Removed handleCancelDelete as it's no longer needed after switching
from ConfirmDialog modal to inline confirmation
2025-10-15 18:30:15 +02:00
Gigi
11925a42b0 style: make trash icon red in delete confirmation
Change from CompactButton to regular button with explicit red color
styling so the trash icon inherits the red color (rgb(220 38 38))
2025-10-15 18:21:44 +02:00
Gigi
acf45530ca refactor: replace delete dialog with inline confirmation
Replace popup modal with inline confirmation UI:
- When delete is clicked, show red trash icon with 'Confirm?' text
- Clicking red trash icon again confirms deletion
- Confirmation appears to left of three-dot menu
- Click outside or reopen menu cancels confirmation
- Remove ConfirmDialog component dependency
2025-10-15 18:15:55 +02:00
Gigi
3792ad6abf refactor: move Highlight Style, Paragraph Alignment, and Default Highlight Visibility to top
Final order:
1. Highlight Style
2. Paragraph Alignment
3. Default Highlight Visibility
4. Reading Font + Font Size
5. My Highlights color
6. Friends Highlights color
7. Nostrverse Highlights color
8. Show highlights checkbox
9. Preview
2025-10-15 17:59:29 +02:00
Gigi
bf98b307e8 style: align setting buttons vertically with fixed label width
Add min-width: 220px to inline setting labels to create consistent
'tab stops' so buttons align vertically regardless of label length.
Remove constraint on mobile where settings stack vertically.
2025-10-15 17:57:33 +02:00
Gigi
d15392f41e refactor: reorder settings with Highlight Style and Paragraph Alignment above Default Highlight Visibility
Final order:
1. Reading Font + Font Size
2. My Highlights color
3. Friends Highlights color
4. Nostrverse Highlights color
5. Highlight Style
6. Paragraph Alignment
7. Default Highlight Visibility
8. Show highlights checkbox
9. Preview
2025-10-15 17:55:58 +02:00
Gigi
f26a024255 refactor: reorder Reading & Display settings
- Highlight Style (first)
- Paragraph Alignment (second)
- Reading Font + Font Size (third)

Better logical grouping with text styling before font selection
2025-10-15 17:54:08 +02:00
Gigi
bf9f894c0d refactor: improve delete dialog UI and simplify message
- Reduce verbose warning text to simple 'This will delete your highlight'
- Add proper CSS styling for confirm dialog with backdrop blur
- Center-aligned text and circular icon with color-coded background
- Modern button styling with proper hover states
- Full-width buttons in action row
- Theme-aware colors using CSS variables
2025-10-15 17:53:26 +02:00
Gigi
53a7b7d1c5 docs: update CHANGELOG.md for v0.6.15 2025-10-15 17:51:38 +02:00
Gigi
a12a883cc6 chore: bump version to 0.6.15 2025-10-15 17:50:47 +02:00
Gigi
0cf076b010 chore: change default paragraph alignment to justify 2025-10-15 17:45:10 +02:00
Gigi
e2c712033f feat: add paragraph alignment setting
- Add paragraphAlignment setting (left/justify) to UserSettings interface
- Add UI control with icon buttons in ReadingDisplaySettings
- Apply alignment via CSS variable to reader content and preview
- Default to left-aligned to maintain current behavior
- Keep headings always left-aligned for better readability
2025-10-15 17:43:31 +02:00
Gigi
e38237ca8e fix: resolve linter errors for unused variables 2025-10-15 17:39:22 +02:00
Gigi
1fff44fc6c chore: update button label from 'Open on Nostr' to 'Open with njump' 2025-10-15 17:37:38 +02:00
Gigi
4e50073e07 feat: update gateway config to use nostr.at and define ants.sh as search portal 2025-10-15 17:31:39 +02:00
Gigi
0ce64fe83f feat: open three-dot menus upward when insufficient space below 2025-10-15 17:28:44 +02:00
Gigi
ef848aa93e feat: update external article menu with Share (Boris link) and Search options 2025-10-15 17:25:29 +02:00
Gigi
67b287d75d feat: add Search option to article menu for ants.sh portal 2025-10-15 17:20:59 +02:00
Gigi
b795dfd2c6 feat: add Copy Link and Copy Original options to article menu 2025-10-15 17:19:20 +02:00
Gigi
c68d855983 feat: add Share and Share Original options to article menu 2025-10-15 17:18:36 +02:00
Gigi
fb1c19e64b docs: update CHANGELOG.md for v0.6.14 2025-10-15 16:30:11 +02:00
Gigi
384c16e29d chore: bump version to 0.6.14 2025-10-15 16:28:55 +02:00
Gigi
789982bd76 Merge pull request #11 from dergigi/bookmarks-reorg
Reorganize bookmarks UI with sections and bookmark sets support
2025-10-15 16:28:25 +02:00
Gigi
8bccc9de48 fix: remove unused articleImage prop from CompactView 2025-10-15 16:27:05 +02:00
Gigi
ec8584b4d2 feat: hide cover images in compact view 2025-10-15 16:25:48 +02:00
Gigi
54bd59fa2d refactor: rename Amethyst-style bookmarks to Old Bookmarks (Legacy) 2025-10-15 16:25:03 +02:00
Gigi
b19f5f55f7 fix: remove borders from compact bookmark cards 2025-10-15 16:21:26 +02:00
Gigi
0964f25f97 refactor: make section dividers more subtle
Changed border color from var(--color-border) to rgba(255, 255, 255, 0.05)
for a much more subtle dividing line between bookmark sections.
2025-10-15 16:20:35 +02:00
Gigi
5f3e6335c1 refactor: reduce section heading bottom padding by half
Changed bottom padding from 0.75rem to 0.375rem for both the section
title and action button to reduce spacing before bookmark items.
2025-10-15 16:20:06 +02:00
Gigi
f30c894c87 fix: align add bookmark button with section heading
- Added matching padding to bookmark-section-action button
- Button now has same vertical padding as section title (1.5rem top, 0.75rem bottom)
- Also handles first section case with reduced padding (0.5rem top)
- Removed unnecessary marginBottom from flex container
2025-10-15 16:19:34 +02:00
Gigi
bec769ac1b refactor: move add bookmark button to web bookmarks section
- Removed add bookmark button from sidebar header
- Added small CompactButton style button next to 'Web bookmarks' heading
- Button only shows when user is logged in and web bookmarks section exists
- Moved bookmark creation logic from SidebarHeader to BookmarkList
- Cleaned up unused imports in SidebarHeader
2025-10-15 16:17:58 +02:00
Gigi
cb3748e06f refactor: remove redundant loading spinner above tabs
Removed the loading spinner that appeared above the tab bar since we now
show spinners in the empty states themselves, making this redundant.
2025-10-15 16:14:04 +02:00
Gigi
d5a24f0a46 refactor: replace empty state messages with spinners
Replaced 'No X yet. Pull to refresh!' messages with spinning loaders for:
- No highlights yet (Me & Explore)
- No bookmarks yet (Me)
- No read articles yet (Me)
- No articles written yet (Me)
- No blog posts yet (Explore)

This provides better UX by showing an active loading state instead of
static empty state messages.
2025-10-15 16:11:05 +02:00
Gigi
401a8241bd fix: resolve lint and type errors
- Changed idToEvent from let to const (prefer-const)
- Fixed TypeScript type narrowing issue by using direct regex test instead of isHexId type guard
- Removed unused isHexId import

All lint and type checks now pass for src directory.
2025-10-15 16:09:46 +02:00
Gigi
2193a7a863 fix: properly handle AddressPointer bookmarks for long-form articles
The issue was that Primal bookmarks long-form articles using 'a' tags
(AddressPointer format: kind:pubkey:identifier) but our code was only
expecting EventPointer objects with 'id' properties.

Changes:
- Updated ApplesauceBookmarks interface to match actual applesauce types
- Added AddressPointer and EventPointer interfaces
- Rewrote processApplesauceBookmarks to handle all bookmark types:
  * notes (EventPointer) - regular notes
  * articles (AddressPointer) - long-form content (kind:30023)
  * hashtags (string[])
  * urls (string[])
- Updated bookmark hydration to query addressable events by coordinates
- Added logging to show hydration stats

This should fix the issue where Primal's Reads bookmarks weren't showing up.
2025-10-15 16:06:03 +02:00
Gigi
e6bc4d7fda chore: update .gitignore 2025-10-15 16:02:30 +02:00
Gigi
aee9f73316 debug: add detailed logging for bookmark event tags
Added logging to show:
- e and a tag counts for all events
- which events survived deduplication
- specific check for Primal reads list (kind:10003 with d='reads')
2025-10-15 15:59:56 +02:00
Gigi
aef7b4cea4 fix: include kind:30003 in default bookmark list detection
Previously, the dedupeNip51Events function was only looking for kind:10003
and kind:30001 when finding the default bookmark list. This excluded
kind:30003 events without a 'd' tag, which is what Primal uses for
bookmarks. Now kind:30003 is properly included in the filter.
2025-10-15 15:54:47 +02:00
Gigi
c9a8a3b91e refactor: remove text shadows from publication date
- Remove text-shadow from CSS for .publish-date-topright
- Remove shadowColor from useAdaptiveTextColor hook
- Only apply adaptive text color, no shadows or backgrounds
- Cleaner appearance with color-based readability only
2025-10-15 15:52:54 +02:00
Gigi
0c7b11bdf8 fix: improve shadow contrast without background overlay
- Increase shadow opacity from 0.5 to 0.8 for better readability
- Revert semi-transparent background approach per user feedback
- Keep debugging logs to diagnose color detection
2025-10-15 15:50:54 +02:00
Gigi
8c151a5855 fix: correct async handling in adaptive color detection
- Remove incorrect await on synchronous getColor method
- Add console logging to debug color detection
- This should fix black-on-black readability issues
2025-10-15 15:49:45 +02:00
Gigi
9b54fa9c14 fix: correct FastAverageColor import to use named export
- Change from default import to named import
- Resolves TypeScript error TS2351
2025-10-15 15:48:02 +02:00
Gigi
99d7705404 feat: add adaptive text color for publication date over images
- Install fast-average-color library for image color detection
- Create useAdaptiveTextColor hook to analyze top-right image corner
- Update ReaderHeader to dynamically adjust date text/shadow colors
- Ensures publication date is readable on both light and dark backgrounds
2025-10-15 15:40:57 +02:00
Gigi
eaa590b8e2 feat: add support for bookmark sets (kind 30003)
- Add setName, setTitle, setDescription, and setImage fields to IndividualBookmark type
- Extract d tag and metadata from kind 30003 events in bookmark processing
- Create helper functions to group bookmarks by set and extract set metadata
- Display bookmark sets as separate sections in BookmarkList UI
- Maintain existing content-type categorization alongside bookmark sets
2025-10-15 15:25:33 +02:00
Gigi
715fd8cf10 refactor: remove duplicate type indicator icon from bookmark cards 2025-10-15 15:11:00 +02:00
Gigi
99a9709605 style: left-align support button, right-align view mode buttons 2025-10-15 15:01:25 +02:00
Gigi
65d330d5ed style: make support heart icon orange using friends color from settings 2025-10-15 14:59:51 +02:00
Gigi
1d1d389a03 feat: move support button to bottom-left of bookmarks bar 2025-10-15 14:58:43 +02:00
Gigi
0392389355 style: change support button icon from lightning bolt to heart 2025-10-15 14:57:24 +02:00
Gigi
cf2a500a07 style(bookmarks): remove border from compact view bookmarks 2025-10-15 14:39:43 +02:00
Gigi
7d3748202e fix(bookmarks): ensure section heading styles override with important 2025-10-15 14:39:00 +02:00
Gigi
d7f90faea9 style(bookmarks): improve section headings with better typography and remove counts 2025-10-15 14:35:18 +02:00
Gigi
cb0066aac9 style(bookmarks): use file-lines icon instead of book for default bookmarks 2025-10-15 14:31:27 +02:00
Gigi
b48397b7a6 feat(bookmarks): use camera icon for image bookmarks 2025-10-15 14:29:35 +02:00
Gigi
82ab8419e3 fix(lint): remove unused variables and fix icon imports 2025-10-15 14:26:02 +02:00
Gigi
142a2414d3 style(bookmarks): use regular icon variants for all classification icons 2025-10-15 14:24:07 +02:00
Gigi
081bd95f60 feat(bookmarks): classify web bookmark URLs to show appropriate content icons 2025-10-15 14:22:13 +02:00
Gigi
300aed0589 style(bookmarks): use regular icon variants for lighter appearance 2025-10-15 14:21:09 +02:00
Gigi
b2b23c66cf feat(bookmarks): add sticky note icon for text-only bookmarks without URLs 2025-10-15 14:20:28 +02:00
Gigi
838bb6aa3d feat(bookmarks): add content type icons to indicate article/video/web 2025-10-15 14:15:01 +02:00
Gigi
f14ecc5acb refactor(bookmarks): simplify filtering to only exclude empty content 2025-10-15 14:14:54 +02:00
Gigi
d533e23dc0 feat(bookmarks): render grouped sections in /me reading-list with global controls 2025-10-15 13:59:10 +02:00
Gigi
eefcf99364 feat(bookmarks): render grouped sections in sidebar with global view mode 2025-10-15 13:59:00 +02:00
Gigi
1c0790bfb6 feat(bookmarks): add grouping and sorting helpers for sections 2025-10-15 13:58:56 +02:00
Gigi
29e351ba78 feat(bookmarks): tag sourceKind in collection for web and list/set items 2025-10-15 13:58:48 +02:00
Gigi
7592c5c327 feat(bookmarks): add sourceKind to IndividualBookmark for grouping 2025-10-15 13:58:41 +02:00
Gigi
f5018204ab docs: update CHANGELOG.md for v0.6.13 release 2025-10-15 12:21:21 +02:00
Gigi
7ae74268fd chore: bump version to 0.6.13 2025-10-15 12:20:13 +02:00
Gigi
52e959a7f5 fix: keep bookmark button visible at top, only hide highlights button
- Bookmark button now visible at top (only hides on scroll down)
- Highlights button hides both at top AND on scroll down
- Separated visibility logic into showBookmarkButton and showHighlightsButton
- Relay status indicator follows bookmark button behavior
2025-10-15 12:19:33 +02:00
Gigi
4f03a2c276 fix: hide highlights button when scrolled to the top
- Highlights button now hidden when at the very top of the page
- Tracks scroll position and hides button when scrollY <= 10px
- Bookmark button remains visible as always
- Highlights button appears when scrolling up from below
- Improves UX by reducing visual clutter at page top
2025-10-15 12:17:44 +02:00
Gigi
bc4c96ee35 feat: add gradient placeholder images for articles without covers
- Blog post cards now show subtle gradient background when no image
- Reader view displays gradient placeholder with newspaper icon
- Large view bookmarks use gradient backgrounds
- Gradients use theme colors (--color-bg-elevated, --color-bg-subtle)
- Placeholder icons have reduced opacity for subtlety
- Adapts automatically to light/dark themes
2025-10-15 12:15:20 +02:00
Gigi
a866040fc1 feat: support nprofile identifiers on /p/ profile pages
- Profile pages now accept both npub and nprofile identifiers (NIP-19)
- Extract pubkey from nprofile.data.pubkey when decoding
- Maintains backward compatibility with existing npub links
- Users can now share profiles with relay metadata included
2025-10-15 12:13:18 +02:00
Gigi
c90fad268a style: improve PWA install section styling in settings
- Add section-title class to heading to match other settings sections
- Replace gradient button with standard zap-preset-btn styling
- Remove inline styles and JavaScript hover effects
- Consistent with app's design system and theme colors
2025-10-15 12:12:12 +02:00
Gigi
8ef1f775f9 fix: improve mobile bookmark button visibility across all pages
- Bookmark button now visible on all pages except settings
- Only hides when scrolling down while reading an article
- Fixes issue where button was hidden on /p/ (profile) and other pages
- Highlights button only shows when viewing article content
- Prevents users from getting stuck without navigation options
2025-10-15 12:10:30 +02:00
Gigi
90af87339c docs: update CHANGELOG.md for v0.6.12 release 2025-10-15 12:06:31 +02:00
Gigi
9007b1ca71 chore: bump version to 0.6.12 2025-10-15 12:05:34 +02:00
Gigi
0b7e6145de style: set horizontal divider opacity to 69% 2025-10-15 11:44:34 +02:00
Gigi
bf1b608d96 style: increase horizontal divider opacity for better visibility 2025-10-15 11:44:04 +02:00
Gigi
7db0f2a05c style: make horizontal dividers more subtle with increased padding 2025-10-15 11:43:33 +02:00
Gigi
165b4d4b9f docs: update CHANGELOG.md for v0.6.11 release 2025-10-15 11:09:16 +02:00
Gigi
a7106138c4 chore: bump version to 0.6.11 2025-10-15 11:08:08 +02:00
Gigi
a498bfab38 feat(mobile): show sidebar buttons on explore page 2025-10-15 11:07:07 +02:00
Gigi
3dd2980283 fix(mobile): memoize toggleSidebar to prevent sidebar from closing immediately on mobile
- Wrapped toggleSidebar in useCallback to prevent function recreation on every render
- Updated route change effect to only close sidebar on actual pathname changes, not state changes
- Fixes issue where bookmarks sidebar wouldn't stay open on mobile PWA
2025-10-15 11:05:17 +02:00
Gigi
2e2a1a2c9d feat: add colored borders to blog post and highlight cards based on relationship
- Add level-based colored borders to blog post cards (mine/friends/nostrverse)
- Updated BlogPostCard to accept and apply level prop
- Modified Explore.tsx to classify blog posts by relationship level
- Added CSS styling using settings colors for visual distinction
- Highlights already had this feature, now writings have it too
2025-10-15 10:32:16 +02:00
Gigi
b9666bf037 docs: update CHANGELOG.md for v0.6.10 release 2025-10-15 10:28:18 +02:00
Gigi
ab1e964d3a chore: bump version to 0.6.10 2025-10-15 10:27:12 +02:00
Gigi
1500744a96 Merge pull request #10 from dergigi/fix-eslint-and-stuff
Improve code quality and explore page UX
2025-10-15 10:26:37 +02:00
Gigi
394311622d refactor: remove subtitle from explore page 2025-10-15 10:22:19 +02:00
Gigi
c7f3991ddd feat: move tabs below filter buttons in explore page 2025-10-15 10:21:26 +02:00
Gigi
e05efaa4f6 feat: make refresh button spin during loading and pull-to-refresh 2025-10-15 10:20:45 +02:00
Gigi
c96347a331 fix: position refresh button next to filter buttons instead of far left 2025-10-15 10:19:52 +02:00
Gigi
d721e84e42 feat: add refresh button to the left of filter buttons in explore page 2025-10-15 10:15:52 +02:00
Gigi
dcbe4bd23e feat: update writings tab icon to use newspaper icon 2025-10-15 10:11:40 +02:00
Gigi
e11184426e feat: default explore filter to friends only 2025-10-15 10:09:55 +02:00
Gigi
ebea872c72 feat: move filter icon buttons to the right in explore page 2025-10-15 10:09:08 +02:00
Gigi
8e57d3d491 refactor: remove unused settings parameter from image cache and bookmark components
- Remove settings parameter from useImageCache and useCacheImageOnLoad hooks as it was never used
- Update all call sites in CardView, CompactView, LargeView, and ReaderHeader
- Remove settings prop from BookmarkItem and its child view components
- Remove settings prop from BookmarkList component
- Update ThreePaneLayout to not pass settings to BookmarkList

This change cascades through the component tree to clean up unused props that were
introduced when we refactored the image caching to use Service Worker instead of
local storage.
2025-10-15 10:01:43 +02:00
Gigi
ca339ac0b2 refactor: remove all eslint-disable statements and fix underlying issues
- Replace @typescript-eslint/no-explicit-any with proper Filter type from nostr-tools/filter in dataFetch.ts and helpers.ts
- Replace @typescript-eslint/no-explicit-any with IAccount and AccountManager types from applesauce-accounts in hooks
- Replace @typescript-eslint/no-explicit-any with unknown type casts in App.tsx for keep-alive subscription
- Fix react-hooks/exhaustive-deps warnings by including all dependencies in useEffect hooks
- Remove unused _settings parameters in useImageCache.ts that were causing no-unused-vars warnings
2025-10-15 09:57:14 +02:00
Gigi
abb6819c40 Merge pull request #9 from dergigi/fix-explore
Fix explore page data loading and simplify fetch/write paths
2025-10-15 09:53:07 +02:00
Gigi
de314894ff fix(explore): properly fix react-hooks/exhaustive-deps warning
Instead of suppressing the warning, use functional setState updates to
check current state without creating dependencies. This allows the effect
to check if blogPosts/highlights are empty without adding them as
dependencies, which would cause infinite re-fetch loops.

The pattern prev.length === 0 ? cached : prev ensures we only seed from
cache on initial load, not on every refresh.
2025-10-15 09:44:57 +02:00
Gigi
2939747ebf fix(lint): resolve all linting and type errors
- Remove unused imports (useRef, faExclamationCircle, getProfileUrl, Observable, UserSettings)
- Remove unused error state and setError calls in Explore and Me components
- Remove unused 'events' variable from exploreService and nostrverseService
- Remove unused '_relays' parameter from saveSettings
- Remove unused '_settings' parameter from publishEvent
- Update all callers of publishEvent and saveSettings to match new signatures
- Add eslint-disable comment for intentional dependency omission in Explore
- Update BookmarkList to use new pull-to-refresh library and RefreshIndicator
- All type checks and linting now pass
2025-10-15 09:42:56 +02:00
Gigi
a4548306e7 fix(ui): update HighlightsPanel to use new pull-to-refresh library
- Replace old usePullToRefresh hook with use-pull-to-refresh library
- Update to use RefreshIndicator component
- Remove ref-based implementation in favor of simpler library API
2025-10-15 09:37:03 +02:00
Gigi
f16c1720a6 fix(ui): remove blocking error screens, show progressive loading with skeletons
- Remove full-screen error messages in Explore and Me
- Show skeletons while loading if no data cached
- Display empty states with 'Pull to refresh!' message
- Allow users to pull-to-refresh to retry on errors
- Keep content visible as data streams in progressively
2025-10-15 09:34:46 +02:00
Gigi
5b2ee94062 feat(ui): replace custom pull-to-refresh with use-pull-to-refresh library for simplicity
- Remove custom usePullToRefresh hook and PullToRefreshIndicator
- Add use-pull-to-refresh library dependency
- Create simple RefreshIndicator component
- Apply pull-to-refresh to Explore and Me screens
- Simplify implementation while maintaining functionality
2025-10-15 09:32:25 +02:00
Gigi
3091ad7fd4 feat(write): add unified publishEvent service and refactor highlight and settings to use it 2025-10-15 09:29:54 +02:00
Gigi
5b7488295c refactor(fetch): migrate nostrverseService, bookmarkService, and libraryService to use queryEvents 2025-10-15 09:28:35 +02:00
Gigi
bea62ddc4b refactor(fetch): migrate exploreService and fetchHighlightsFromAuthors to use queryEvents 2025-10-15 09:26:33 +02:00
Gigi
44d6b1fb2a fix(explore): prevent refresh loop and avoid false empty-follows error; always load nostrverse 2025-10-15 09:19:10 +02:00
Gigi
02ec8dd936 feat(fetch): add unified queryEvents helper and stream contacts partials with extended timeout 2025-10-15 09:17:51 +02:00
Gigi
765ce0ac5e feat(network): centralize relay timeouts and contacts remote timeout 2025-10-15 09:15:48 +02:00
Gigi
a1f7c3e34a Merge pull request #8 from dergigi/support
Add support page with zap receipt display
2025-10-15 01:54:47 +02:00
Gigi
2e5eb08b54 fix: simplify copy 'show up above' to 'show above' 2025-10-15 01:51:24 +02:00
Gigi
46a6d4fe0c chore: restore production thresholds (2100 sats / 69420 sats)
Removed testing values and restored proper production thresholds
2025-10-15 01:44:45 +02:00
Gigi
84ea0df550 refactor: improve support page spacing and visual hierarchy
- Increase hero section spacing (mb-16/20 instead of mb-8/12)
- Larger illustration (w-56/72 instead of w-48/64)
- Bigger heading (text-4xl/5xl instead of 3xl/4xl)
- More generous section spacing throughout
- Larger gaps between avatars (gap-8/10 for legends)
- More columns on larger screens (lg breakpoint added)
- Add subtle border-top separator before footer
- Increase footer spacing (mt-16/20)
- Larger call-to-action text (text-base)
- Change 'Absolute Legends' to 'Legends' (shorter, cleaner)
2025-10-15 01:43:12 +02:00
Gigi
0f58b166ce refactor: make regular supporter avatars smaller and remove border
- Reduced from w-12 h-12 md:w-16 md:h-16 to w-10 h-10 md:w-12 md:h-12
- Removed ring border for regular supporters (keep yellow ring for whales only)
- Simplified styling logic
2025-10-15 01:40:34 +02:00
Gigi
f65d39023c refactor: reorder footer text and make stats smaller
- Move call-to-action above stats
- Reduce stats font size from text-sm to text-xs
- Make call-to-action more prominent
2025-10-15 01:39:57 +02:00
Gigi
0b3c7efbc1 refactor: remove superfluous 'Want to show up here?' question
Make call-to-action more direct and concise
2025-10-15 01:38:58 +02:00
Gigi
ecb462562f feat: link 'Boris' to njump.me profile page
Allow users to easily navigate to Boris profile to send zaps
2025-10-15 01:38:34 +02:00
Gigi
c5a3d00371 feat: link 'meaningful amount of sats' to pricing page
Makes the call-to-action more actionable by linking to payment info
2025-10-15 01:36:29 +02:00
Gigi
d3b7a8ddde feat: add call-to-action message at bottom of support page
Encourage visitors to zap Boris to show up on the supporter list
2025-10-15 01:36:12 +02:00
Gigi
0eee203a9b feat: link 'zaps' to pricing page
Add clickable link on 'zaps' text pointing to https://www.readwithboris.com/#pricing
Helps users learn how to send zaps to support the project
2025-10-15 01:34:35 +02:00
Gigi
cd5a95dea3 refactor: reduce regular supporter avatar size
Changed from w-16 h-16 md:w-20 md:h-20 to w-12 h-12 md:w-16 md:h-16
Makes regular supporters more compact while keeping legends prominent
2025-10-15 01:34:05 +02:00
Gigi
f348ddaf73 revert: restore 'Absolute Legends' heading
Changed back from 'Legends' to 'Absolute Legends'
2025-10-15 01:33:44 +02:00
Gigi
9f09093c80 refactor: simplify whale section heading to 'Legends'
Changed from 'Absolute Legends' to 'Legends' for brevity
2025-10-15 01:30:52 +02:00
Gigi
490c6c9bdc feat: make supporter avatars clickable to view profiles
- Click avatar to navigate to /p/:npub profile page
- Add hover scale effect for visual feedback
- Convert pubkey to npub for navigation
2025-10-15 01:29:57 +02:00
Gigi
4eb0ede76b refactor: remove bolt icon from Absolute Legends header
Keep the bolt badge on whale profile pictures only
2025-10-15 01:29:11 +02:00
Gigi
02c1b6b783 chore: temporarily lower thresholds for testing (2 sats / 21 sats)
- Lower supporter threshold from 2100 to 2 sats
- Lower whale threshold from 69420 to 21 sats
- Add TODO comment to restore production values
- For testing support page with smaller zaps
2025-10-15 01:25:39 +02:00
Gigi
9eed448da6 feat: improve support page copy and add thank-you illustration
- Add exclamation mark to 'Thank You!' heading for warmth
- Simplify description text (remove redundant thank you)
- Rename 'Mega Supporters' to 'Absolute Legends' for more fun tone
- Add thank-you.svg illustration asset
2025-10-15 01:25:00 +02:00
Gigi
f8d621bcdc fix: change support page heading to 'Thank You' 2025-10-15 01:22:17 +02:00
Gigi
5cbe2246d3 fix: apply proper theme colors to support page for readability
- Use CSS variables for background, text, and border colors
- Add min-h-screen wrapper with proper background color
- Replace hardcoded zinc colors with theme-aware variables
- Ensure text is readable in both light and dark themes
2025-10-15 01:21:57 +02:00
Gigi
f29a180cbd feat: add thank-you illustration to support page 2025-10-15 01:20:05 +02:00
Gigi
0ca3771906 fix: improve zap receipt scanning with applesauce helpers and more relays
- Use isValidZap, getZapSender, getZapAmount from applesauce-core/helpers
- Add common zap relays (Mutiny, Alby) to improve coverage
- Add detailed logging to debug zap receipt fetching
- Remove custom extraction functions in favor of applesauce helpers
2025-10-15 01:04:17 +02:00
Gigi
6dab126f88 fix: resolve lint and type errors in support page implementation 2025-10-15 01:01:18 +02:00
Gigi
6c74d04984 docs: add Support page section to FEATURES.md 2025-10-15 00:54:03 +02:00
Gigi
1e00ff5e35 feat: add Support button (bolt icon) to SidebarHeader navigation 2025-10-15 00:53:34 +02:00
Gigi
71fa334f61 feat: add /support route to App routing 2025-10-15 00:53:30 +02:00
Gigi
d3ee995221 feat: wire Support component into Bookmarks with /support detection 2025-10-15 00:53:26 +02:00
Gigi
6812584b8c feat: extend ThreePaneLayout with showSupport and support slot 2025-10-15 00:53:22 +02:00
Gigi
47ddf8ebe1 feat: add Support component to display zappers with avatar grid 2025-10-15 00:53:18 +02:00
Gigi
36897e7f15 feat: add zapReceiptService to fetch and aggregate kind:9735 receipts 2025-10-15 00:53:14 +02:00
Gigi
f18315be02 feat: export BORIS_PUBKEY for reuse in support page 2025-10-15 00:53:09 +02:00
Gigi
38d77b02f5 docs: add FEATURES.md summarizing app features 2025-10-14 22:49:45 +02:00
Gigi
5b77a93bba chore: add MIT License 2025-10-14 16:53:59 +02:00
Gigi
e1c11a7450 docs: update CHANGELOG.md for v0.6.9 2025-10-14 16:50:40 +02:00
Gigi
d96ee50f5a chore: bump version to 0.6.9 2025-10-14 16:48:32 +02:00
Gigi
d4a172ba7e docs: update CHANGELOG.md for v0.6.8 2025-10-14 16:40:08 +02:00
Gigi
52ddb8dd7d chore: bump version to 0.6.8 2025-10-14 16:39:08 +02:00
Gigi
8c16614752 chore: update favicon and app icons to purple theme 2025-10-14 16:38:21 +02:00
Gigi
700d7cc5fa chore: update favicon and app icons 2025-10-14 16:34:21 +02:00
Gigi
017703dab2 fix: use consistent yellow color (#fde047) for default highlight settings 2025-10-14 16:31:55 +02:00
Gigi
c59fdb14f1 docs: update CHANGELOG.md for v0.6.7 2025-10-14 15:46:21 +02:00
Gigi
0c104f95d9 chore: bump version to 0.6.7 2025-10-14 15:44:25 +02:00
Gigi
acbefae501 Merge pull request #7 from dergigi/loading-placeholders
Remove loading spinners in favor of skeleton placeholders
2025-10-14 15:43:55 +02:00
Gigi
2ce83ef88a fix: use React.ReactElement instead of JSX.Element type
Change return type from JSX.Element to React.ReactElement to fix ESLint no-undef error
2025-10-14 15:42:54 +02:00
Gigi
dab3412ecd refactor: remove loading spinner from explore page
Remove incremental loading spinner as pull-to-refresh indicator already provides visual feedback for refresh state. Initial loading continues to use skeleton placeholders.
2025-10-14 15:41:34 +02:00
Gigi
988b3164d2 docs: add loading placeholder guideline to fontawesome rule 2025-10-14 15:40:32 +02:00
Gigi
4161053821 fix: Me - handle undefined viewingPubkey in skeleton loading state 2025-10-14 15:37:03 +02:00
Gigi
60054c4865 feat: ContentPanel - replace spinner with skeleton loaders 2025-10-14 15:36:57 +02:00
Gigi
f4e8aa576c feat: HighlightsPanel - replace spinner with skeleton loaders 2025-10-14 15:35:28 +02:00
Gigi
30a495bcd1 feat: Me - replace spinner with skeleton loaders 2025-10-14 15:35:23 +02:00
Gigi
6dde0eb220 feat: Explore - replace spinner with skeleton loaders 2025-10-14 15:35:17 +02:00
Gigi
90d8ef3423 feat: BookmarkList - replace spinner with skeleton loaders 2025-10-14 15:35:10 +02:00
Gigi
f626a8ec9b feat: add skeleton components and theme provider 2025-10-14 15:35:03 +02:00
Gigi
a7c7535236 feat: add react-loading-skeleton package 2025-10-14 14:53:40 +02:00
Gigi
5b0f2821d6 feat: parse and render nostr identifiers in highlight comments
- Detect and decode nostr: URIs (npub, nprofile, naddr, note, nevent) in comments
- Render profiles as clickable links with shortened pubkeys (@abc12345...)
- Render blog posts (kind:30023) as clickable article links
- Shorten other event identifiers to prevent layout breaks
- Add monospace styling for shortened nostr IDs
- Maintains DRY principles by extending existing CommentContent component
2025-10-14 12:58:01 +02:00
Gigi
be045557b8 feat: add nostrverse content and visibility filters to explore page
- Add visibility filter state and UI (mine/friends/nostrverse toggles)
- Create nostrverseService to fetch public content from the entire network
- Fetch both friends content and nostrverse content in parallel
- Apply visibility filters to both highlights and blog posts
- Filter buttons match highlight sidebar styling
- Users can now discover content beyond their friend network
- Maintains performance with sensible limits (50 posts, 100 highlights)
2025-10-14 12:09:12 +02:00
Gigi
a0c92182f9 docs: update CHANGELOG.md for v0.6.6 release 2025-10-14 12:03:53 +02:00
Gigi
f33d33556b chore: bump version to 0.6.6 2025-10-14 12:02:20 +02:00
Gigi
9aff889835 fix: correct profile fetching implementation and dependencies
- Use eventStore.add() directly instead of mapEventsToStore
- Use tap() operator to process and store events as they arrive
- Add eventStore and settings to useEffect dependencies
- Fixes TypeScript and ESLint errors
2025-10-14 12:00:52 +02:00
Gigi
420df1fbdd feat: fetch and cache author profiles in explore page
- Create profileService to fetch and cache kind:0 metadata
- Fetch profiles for all blog post authors on explore page
- Store profiles in event store for immediate access
- Rebroadcast profiles to local/all relays per user settings
- Fixes 'Unknown' author names by ensuring profiles are cached
- Uses mapEventsToStore to automatically populate event store
2025-10-14 11:59:28 +02:00
Gigi
2946ede5ac fix: filter out blog posts with far-future publication dates
- Add filteredBlogPosts useMemo to exclude posts with unreasonable dates
- Allow 1 day into future for clock skew tolerance
- Prevents spam/error posts with dates like '53585 years from now'
- Uses published_at tag or event.created_at as fallback
2025-10-14 11:57:04 +02:00
Gigi
6ec28e6a9d feat: render links and images in highlight comments
- Parse URLs in comment text and render as clickable links
- Detect image URLs and render inline images
- Add CommentContent component for smart URL rendering
- Style links with primary color and underline
- Style images with border and rounded corners
- Images lazy-load and respect max-width
- Links open in new tab with noopener/noreferrer
2025-10-14 11:54:41 +02:00
Gigi
820daa489e feat: hide citation in highlights sidebar for current article
- Add showCitation prop to HighlightItem (defaults to true)
- Set showCitation={false} in HighlightsPanel
- Reduces redundancy since all sidebar highlights are from same article
- Citation still shown in Explore and Me pages where context is needed
2025-10-14 11:52:29 +02:00
Gigi
b162596013 fix: prevent layout breaks from long URLs in highlight comments
- Add word-wrap, overflow-wrap, and word-break to comments
- Set min-width: 0 to allow flex child to shrink
- Prevents horizontal overflow from long URLs or text
- Maintains readable layout with line wrapping
2025-10-14 11:51:16 +02:00
Gigi
e581237e16 docs: update CHANGELOG.md for v0.6.5 release 2025-10-14 11:49:43 +02:00
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
Gigi
7464a8b505 chore: bump version to 0.6.3 2025-10-14 01:27:22 +02:00
Gigi
938d79663b fix: remove unused isRefreshing parameter from PullToRefreshIndicator
- Keep prop in interface for backward compatibility
- Don't destructure unused parameter to satisfy linter
- All lint checks and type checks now pass
2025-10-14 01:13:02 +02:00
Gigi
cc0ad69275 chore: remove COLOR_SYSTEM.md documentation file 2025-10-14 01:11:32 +02:00
Gigi
810ff060f8 feat: make relay status indicator circular FAB on mobile
- Default to collapsed (icon only) on mobile
- Expand to show details when tapped on mobile
- Circular 56px FAB when collapsed, matching highlight button style
- Desktop always shows expanded with details
- Hide on scroll via showOnMobile prop (matches sidepanel buttons)
2025-10-14 01:11:14 +02:00
Gigi
5e03ef70a6 style: improve relay status indicator text layout
- Make subtitle text smaller (0.75rem) with reduced opacity
- Display text in column layout with proper line spacing
- Subtitle now appears on second line below title
- Apply consistent styling to offline and flight mode subtitles
2025-10-14 01:08:28 +02:00
Gigi
f05fb29c7b refactor: remove refresh spinner and text from pull-to-refresh
- Remove 'Refreshing...' text from indicator
- Remove spinner from pull-to-refresh (button already spins)
- Only show indicator when actively pulling, not when refreshing
- Simplify logic and improve UX consistency
2025-10-14 01:05:22 +02:00
Gigi
e737b1f7f0 fix: position relay status indicator in bottom-left corner
- Add fixed positioning (bottom-left) to match highlight button (bottom-right)
- Add modern styling with semi-transparent background, blur, and shadow
- Ensure proper visibility on mobile with smooth transitions
- Maintain responsive behavior for expanded/collapsed states
2025-10-14 01:04:18 +02:00
Gigi
21a7be2f98 fix: unify highlight visibility button styling across app
- Remove blue primary variant from highlight filter buttons
- Use opacity (1.0 active, 0.4 inactive) instead of variant change
- Update settings to use IconButton like sidebar (DRY)
- Consistent styling: ghost variant + opacity + custom colors
- Settings buttons now match sidebar buttons exactly
2025-10-14 01:02:46 +02:00
Gigi
4c720aa049 feat: add public profile pages at /p/:npub
- Make AuthorCard clickable to navigate to user profiles
- Add /p/:npub and /p/:npub/writings routes
- Reuse Me component for public profiles with pubkey prop
- Show highlights and writings tabs for any user
- Hide private tabs (reading-list, archive) on public profiles
- Public profiles show only public data (highlights, writings)
- Private data (bookmarks, read articles) only visible on own profile
- Add clickable author card hover styles with indigo border
- Decode npub to pubkey for profile viewing
- DRY: Single Me component serves both /me and /p/:npub routes
2025-10-14 01:01:10 +02:00
Gigi
6b240b01ec docs: update changelog for v0.6.2 2025-10-14 00:54:28 +02:00
Gigi
945894e3db chore: bump version to 0.6.2 2025-10-14 00:53:17 +02:00
Gigi
667397e528 fix: align title, summary, meta, and body text in reader
- Add consistent 2rem horizontal padding to reader-header on desktop
- Apply same padding to reader-summary-below-image, article-menu-container, and mark-as-read-container
- All content elements now align properly with body text
- Mobile (< 769px) retains base padding only
2025-10-14 00:52:19 +02:00
Gigi
e4b0d6d1cd refactor: unify button styles across sidebars using IconButton
- Convert HighlightsPanel buttons to use IconButton component
- Add style prop support to IconButton for custom styling
- Remove redundant CSS for old button classes (level-toggle-btn, refresh-highlights-btn, etc.)
- Keep only highlight-level-toggles container styling
- Consistent button appearance across left and right sidebars
- DRY: Single IconButton component handles all sidebar buttons
2025-10-14 00:50:52 +02:00
Gigi
3cdda2dcb7 refactor: move bookmark refresh button to footer with view controls
- Remove separate refresh section from bookmarks list
- Add refresh button to view-mode-controls footer
- Show last update time in button tooltip instead of inline text
- Cleaner UI with all controls in one footer section
2025-10-14 00:48:47 +02:00
Gigi
876ecc808d feat: add pull-to-refresh for mobile on all scrollable views
- Create reusable usePullToRefresh hook with touch gesture detection
- Add PullToRefreshIndicator component with visual feedback
- Implement pull-to-refresh on HighlightsPanel (right sidebar)
- Implement pull-to-refresh on Explore page
- Implement pull-to-refresh on Me pages (all tabs)
- Implement pull-to-refresh on BookmarkList (left sidebar)
- Only activates on touch devices for mobile-first experience
- Shows rotating arrow icon that becomes refresh spinner
- Displays contextual messages (pull/release/refreshing)
- Integrates with existing refresh handlers and loading states
2025-10-14 00:47:48 +02:00
Gigi
34671bd067 feat: add three-dot menu for external URLs in /r/ path
- Add menu button with options to open original URL, copy URL, and share
- Reuse existing menu styling for consistency
- Menu positioned at end of article content before mark-as-read button
2025-10-14 00:39:53 +02:00
Gigi
a6285f6a1d fix(highlights): precise normalized-to-original mapping to eliminate intra-word spaces
- Walk original text across node boundaries while tracking normalized positions
- Identify exact start/end nodes and offsets for the match
- Compute combined indices from node spans to create accurate DOM Range
- Eliminates artifacts like 'We b' by preventing whitespace from splitting words
- Keeps strict bounds checks and graceful failures
2025-10-14 00:37:56 +02:00
Gigi
36508d600a fix(highlights): remove existing highlight marks before applying new ones
- Strip all existing mark elements from HTML before re-highlighting
- Prevents old broken highlights from persisting in the DOM
- Ensures clean text is used as the base for new highlight application
- Fixes 'We b' spacing issue caused by corrupted marks from previous buggy renders
- Remove debug logging now that position mapping is working correctly
2025-10-14 00:34:55 +02:00
Gigi
a304bb7c26 debug: add detailed position mapping logging to diagnose spacing issues
- Log search text, match indices, and extracted text during position mapping
- Show sample of combined text around the extracted range
- Help identify where position mapping is going wrong for 'We b' issue
2025-10-14 00:31:59 +02:00
Gigi
04bab96a07 fix(highlights): improve normalized text position mapping to prevent character spacing issues
- Build explicit position map array from normalized to original text indices
- Properly handle whitespace sequences in position mapping
- Ensure each normalized character position maps to correct original position
- Validate mapped positions are within bounds before using
- Fixes spacing issues like 'We b' appearing instead of 'Web' in highlights
2025-10-14 00:27:38 +02:00
Gigi
22ebbff755 fix(highlights): add text validation before applying highlights
- Validate extracted range text matches search text before highlighting
- Check single-node matches are not empty or whitespace-only
- Compare both exact and normalized text to handle whitespace variations
- Prevent broken/corrupted highlights from being applied to DOM
- Add detailed logging for validation failures to aid debugging
2025-10-14 00:25:18 +02:00
Gigi
b43f40597f fix(highlights): add robust validation and error handling for multi-node highlighting
- Implement proper normalized-to-original text position mapping
- Add comprehensive validation for range indices and node offsets
- Verify range is not collapsed before extracting content
- Add try-catch block to handle DOM manipulation errors gracefully
- Add detailed warning logs for debugging failed highlight matches
- Prevent invalid ranges from corrupting the DOM structure
- Fix broken text nodes and visual artifacts in highlighted content
2025-10-14 00:23:43 +02:00
Gigi
fe3af25c5f docs: update changelog for v0.6.1 release 2025-10-14 00:21:31 +02:00
Gigi
ffafc6f64d chore: bump version to 0.6.1 2025-10-14 00:20:37 +02:00
Gigi
eadab9a37f fix(highlights): restore scroll-to-highlight functionality with MutationObserver
- Add MutationObserver to detect when highlights are added/removed from DOM
- Use contentVersion state to trigger re-attachment of click handlers
- Add contentVersion dependency to scroll effect so it re-runs after DOM updates
- Add 100ms delay to scroll effect to ensure DOM is fully rendered
- Add warning log when mark element cannot be found
- Fixes issue where clicking highlights in sidebar wouldn't scroll after DOM changes
2025-10-14 00:19:45 +02:00
Gigi
13b1692931 fix(highlights): create single continuous highlight element for multi-node selections
- Use DOM Range API to extract and wrap content in a single mark element
- Preserves internal DOM structure (links, formatting) within the highlight
- Eliminates visual breaks between multiple mark elements
- Ensures highlight appears as one continuous selection even across inline elements
2025-10-14 00:18:30 +02:00
Gigi
3a78289fee fix(highlights): make multi-node highlights appear seamless
- Remove border-radius (set to 0) to eliminate rounded corner breaks
- Remove horizontal padding (only 0.1rem vertical for slight breathing room)
- Remove all box-shadows that create visual separation
- Simplify hover states for cleaner appearance
- Highlights spanning multiple DOM nodes now appear as one continuous highlight
2025-10-14 00:16:37 +02:00
Gigi
12c70b06de fix(highlights): improve text matching to handle multi-node selections
- Add tryMultiNodeMatch function to find text spanning multiple DOM nodes
- Build combined text from all text nodes for comprehensive matching
- Handle highlighting across node boundaries with proper offsets
- Falls back to multi-node matching when single-node match fails
- Fixes issue where selections with inline formatting couldn't be matched
2025-10-14 00:05:43 +02:00
Gigi
c7c82954ad feat(highlights): force synchronous render for immediate highlight display
- Use flushSync to force React to render synchronously after highlight creation
- Eliminates render cycle delay for instant visual feedback
- Highlights now appear immediately in the text when created
2025-10-14 00:03:56 +02:00
Gigi
3b639e2783 feat(highlights): clear browser selection immediately after creating highlight
- Add window.getSelection().removeAllRanges() after highlight creation
- Ensures DOM can update immediately without selection interference
- Improves perceived responsiveness of highlight creation
2025-10-14 00:03:19 +02:00
Gigi
946584236d fix(mobile): prevent horizontal overflow from code blocks and wide content
- Add mobile-specific styles for pre/code elements with word-wrap
- Use pre-wrap for code blocks to wrap long lines on mobile
- Add max-width and overflow-x constraints to main pane container
- Add overflow-x: hidden to body to prevent horizontal scrolling
- Handle tables and images with max-width: 100% on mobile
- Ensure all content respects viewport width on mobile devices
2025-10-13 23:58:39 +02:00
Gigi
aadbf2084f fix(mobile): hide sidebar/highlights toggle buttons on settings, explore, and me pages
- Only show mobile floating buttons when viewing article content
- Hide buttons on settings/explore/me views to avoid UI clutter
- Update conditional rendering logic in ThreePaneLayout
2025-10-13 23:57:06 +02:00
Gigi
3d7b649cba fix(ui): prevent long relay URLs from causing horizontal overflow on mobile
- Add relay-url className to relay URL elements
- Override inline nowrap styles on mobile with word-break: break-all
- Allow relay URLs to wrap across multiple lines on mobile
- Prevent horizontal overflow from long relay names like proxy URLs
2025-10-13 23:55:35 +02:00
Gigi
caa07012a7 fix(ui): make settings view mobile-friendly
- Add max-width: 100% and overflow handling to preview content
- Add word-break and overflow-wrap to prevent text overflow
- Make inline settings stack vertically on mobile
- Reduce padding on mobile for settings view
- Make zap preset buttons flex to fit mobile width
- Ensure all setting controls respect viewport width
- Fix preview heading size on mobile
2025-10-13 23:53:43 +02:00
Gigi
ad5cd875de feat(ui): improve zap splits settings styling
- Add styled preset buttons (Default, Generous, Selfless, Boris)
- Make sliders 100% width with full-width container
- Style active preset button with indigo-500
- Add hover effects to preset buttons and slider thumbs
- Improve slider thumb appearance with rounded design
- Style description box with proper background and borders
- Use Tailwind colors throughout (zinc, indigo)
2025-10-13 23:50:00 +02:00
Gigi
0a4bc2cfbb fix(ui): show filename for videos instead of 'Error Loading Content'
- Add getFilenameFromUrl helper to extract filename from URL
- Use filename as title when content loading fails (e.g., for video files)
- Decode URI component to handle special characters in filenames
- Improves UX for video and media file viewing
2025-10-13 23:48:11 +02:00
Gigi
605dd41939 fix(ui): render AddBookmarkModal using portal to fix z-index stacking
Use React createPortal to render modal directly to document.body, bypassing the sidebar's stacking context (z-index: 1) which was preventing the modal from appearing above other elements
2025-10-13 23:46:50 +02:00
Gigi
8679ae7a37 feat(ui): increase horizontal padding for reader text content on desktop
Add 2rem left and right padding to reader-html and reader-markdown on desktop (min-width: 769px) for better text spacing from edges
2025-10-13 23:43:28 +02:00
Gigi
3c1e4312c9 feat(me): add Writings tab to display user's published articles
- Add 'writings' tab type to Me component
- Fetch articles written by logged-in user using fetchBlogPostsFromAuthors
- Display writings in same grid style as archive tab
- Add pen-to-square icon for writings tab
- Add /me/writings route
- Update Bookmarks component to handle writings tab routing
- Show article count in tab badge
- Empty state message for users with no published articles
2025-10-13 23:42:26 +02:00
Gigi
53ed6849af feat(ui): add vertical padding to blockquotes
Add 1rem top and bottom padding to blockquotes for better spacing and visual separation
2025-10-13 23:38:14 +02:00
Gigi
4b95e6c262 feat(ui): add comprehensive list styling for articles
- Add proper ul/ol styling with disc and decimal markers
- Add 2rem left padding for list indentation
- Add proper spacing between list items (0.375rem)
- Style nested lists with circle (ul) and lower-alpha (ol)
- Reduce margins for nested lists
- Handle paragraphs within list items with reduced margins
- Use zinc-200 color for list items
- Support both markdown and HTML content
2025-10-13 23:37:32 +02:00
Gigi
40ab215c4d refactor(ui): simplify blockquote styling to minimal indent and italic
- Remove colored left border
- Remove background color
- Remove border radius and padding
- Keep only 2rem left padding for indentation
- Keep italic style for differentiation
- Cleaner, more minimal appearance
2025-10-13 23:36:28 +02:00
Gigi
823927525f feat(ui): add comprehensive headline styling with Tailwind typography
- Add proper h1-h6 styling for both markdown and HTML content
- Use Tailwind font sizes: h1 (text-4xl), h2 (text-3xl), h3 (text-2xl), h4 (text-xl), h5 (text-lg), h6 (text-base)
- Apply appropriate font weights: h1 (700), h2-h6 (600)
- Use zinc-100 for h1-h3, zinc-200 for h4-h6 for proper hierarchy
- Add proper top and bottom margins for better spacing
- Set line heights for optimal readability
2025-10-13 23:35:38 +02:00
Gigi
6277824b32 feat(ui): add blockquote styling with Tailwind colors
- Add blockquote styles for both markdown and HTML content
- Use indigo-500 left border for visual distinction
- Use zinc-800 background for subtle emphasis
- Add proper spacing and rounded corners
- Apply zinc-300 color and italic style for readability
- Properly handle nested paragraph margins
2025-10-13 23:34:30 +02:00
Gigi
f94e4ba900 feat(ui): make article titles larger and show summaries
- Increase title font size to 2.5rem (desktop) and 2rem (mobile)
- Add font-weight: 700 and better line-height to titles
- Increase summary font size to 1.2rem with better line-height
- Fix missing summary display by passing summary prop to ReaderHeader
- Improve readability and visual hierarchy of article headers
2025-10-13 23:33:23 +02:00
Gigi
acf14ccee9 feat(ui): add 100vh height and drop-shadows to sidebars
- Set sidebars to always have 100vh height on desktop
- Add drop-shadow to left sidebar (2px right shadow)
- Add drop-shadow to right highlights panel (2px left shadow)
- Improves visual separation and depth perception
2025-10-13 23:28:00 +02:00
Gigi
f882b63359 fix(ui): remove padding gaps around sidebars
Remove md:p-4 padding from root container to eliminate gaps between screen edges and sidebars
2025-10-13 23:26:25 +02:00
Gigi
7b1e3be39b fix(api): resolve TypeScript errors in video-meta.ts
- Remove non-existent getVideoDetails import from youtube-caption-extractor
- Add Subtitle type definition for proper type conversion
- Remove invalid 'auto' parameter from getSubtitles call
- Convert Subtitle[] to Caption[] with proper type casting
- Simplify title/description extraction for YouTube metadata
2025-10-13 23:23:52 +02:00
Gigi
ee17018076 docs: add comprehensive color system documentation 2025-10-13 23:19:57 +02:00
Gigi
1dd2e1dc38 refactor: switch to brighter yellow-300 for highlight defaults and add semantic color aliases 2025-10-13 23:19:33 +02:00
Gigi
4cd1aa89ad chore: remove migration plan file 2025-10-13 23:18:26 +02:00
Gigi
e667cf05c2 refactor: update default highlight color to yellow-400 in useSettings 2025-10-13 23:18:13 +02:00
Gigi
7512375728 refactor: update default highlight color to yellow-400 in ThreePaneLayout 2025-10-13 23:17:46 +02:00
Gigi
f108e2e70a refactor: replace arbitrary color values with Tailwind utilities in ThreePaneLayout 2025-10-13 23:17:21 +02:00
Gigi
daa43ec4c4 refactor: migrate global.css to Tailwind color palette 2025-10-13 23:16:51 +02:00
Gigi
ab2223e739 refactor: migrate app.css to Tailwind color palette 2025-10-13 23:16:28 +02:00
Gigi
e8cbb3af4b refactor: migrate legacy.css to Tailwind color palette 2025-10-13 23:16:06 +02:00
Gigi
f374a9af28 refactor: migrate settings.css to Tailwind color palette 2025-10-13 23:15:40 +02:00
Gigi
f2cbc66a97 refactor: migrate profile.css to Tailwind color palette 2025-10-13 23:15:21 +02:00
Gigi
1627d4f53e refactor: migrate me.css to Tailwind color palette 2025-10-13 23:14:56 +02:00
Gigi
b93a4d072a refactor: migrate reader.css to Tailwind color palette 2025-10-13 23:14:02 +02:00
Gigi
3e4bc97684 refactor: migrate toast.css to Tailwind color palette 2025-10-13 23:12:48 +02:00
Gigi
3c0c20f61c refactor: migrate modals.css to Tailwind color palette 2025-10-13 23:12:26 +02:00
Gigi
dae63e210b refactor: migrate forms.css to Tailwind color palette 2025-10-13 23:11:42 +02:00
Gigi
dc500cc296 refactor: migrate cards.css to Tailwind color palette 2025-10-13 23:11:03 +02:00
Gigi
1fc1e4f102 refactor: migrate icon-button.css to Tailwind color palette 2025-10-13 23:08:31 +02:00
Gigi
524b5e1559 refactor: migrate sidebar.css to Tailwind color palette 2025-10-13 23:07:53 +02:00
Gigi
930de76d1f refactor: migrate highlights.css to Tailwind color palette 2025-10-13 23:06:09 +02:00
Gigi
b85fc820d1 refactor: setup Tailwind color foundation with semantic aliases and updated defaults 2025-10-13 23:03:07 +02:00
Gigi
b145aee29d docs: add comprehensive Tailwind color migration plan 2025-10-13 22:54:22 +02:00
Gigi
a0e65a48f1 refactor: clean up legacy.css removing unused debugging styles 2025-10-13 22:51:49 +02:00
Gigi
ccdfc54cdc style: increase spacing between highlight card header/footer and content 2025-10-13 22:49:37 +02:00
Gigi
61ce338b8c fix: show reading progress indicator on mobile at full width 2025-10-13 22:48:39 +02:00
Gigi
47de9a75b7 style: remove rounded corners from bookmark sidebar header and fix profile avatar size 2025-10-13 22:48:23 +02:00
Gigi
607f3d46f0 fix: remove sidebar margins and constrain reading progress bar to content pane 2025-10-13 22:44:59 +02:00
Gigi
bdbc08fdf1 style: make mobile sidebar buttons more subtle and refined 2025-10-13 22:40:01 +02:00
Gigi
3a28160ae8 fix: prevent icon blurriness on mobile by setting explicit sizes 2025-10-13 22:38:36 +02:00
Gigi
e03696eed7 style: make sidebars extend edge-to-edge on desktop 2025-10-13 22:37:20 +02:00
Gigi
f80fa3de7f fix: correct highlight card borders and rounded corners 2025-10-13 22:36:04 +02:00
Gigi
4518fc16a7 docs: update changelog for v0.6.0 release 2025-10-13 22:34:09 +02:00
Gigi
7f2b70779b chore: bump version to 0.6.0 2025-10-13 22:33:18 +02:00
Gigi
cc9cc47b51 Merge pull request #5 from dergigi/reading-position
feat: reading position tracking and Tailwind CSS v4 migration
2025-10-13 22:32:52 +02:00
Gigi
a19cb8b6dc fix: remove mobile content pane gap and ensure full width display 2025-10-13 22:26:13 +02:00
Gigi
c564d1608b fix: remove padding on mobile main pane for edge-to-edge content
- Changed mobile .pane.main padding from 0.5rem to 0
- Content now extends fully edge-to-edge on mobile
- Matches design expectation for mobile reading experience
2025-10-13 22:22:24 +02:00
Gigi
c146a8f7ec style: make reading progress indicator smaller and more subtle
- Reduced bar height from 4px to 2px (h-0.5)
- Made container more compact: py-1 instead of py-2
- Tiny text size: 0.625rem (10px) with tabular numbers
- Simplified background: less opacity, lighter blur
- Show just % or checkmark when complete
- Reduced reader bottom padding from 4rem to 2rem
- More minimalist and less intrusive design
2025-10-13 22:20:01 +02:00
Gigi
48cde27a5b refactor: extract legacy styles to dedicated file
- Created src/styles/utils/legacy.css for bookmark/nostr styles
- Reduced index.css from 214 lines to 17 lines
- Fixed duplicate .loading style definitions
- DRY improvement: shared word-break pattern across nostr classes
- Better organization and maintainability
2025-10-13 22:18:12 +02:00
Gigi
fdf0644bbb refactor: massive cleanup of index.css duplicates
- Reduced from 3175 lines to 213 lines (-2960 lines!)
- Removed all reader, bookmark, highlight, settings, modal styles
- These are already imported from modular CSS files
- Only kept truly unique utility classes
- Fixes CSS duplication and specificity issues
2025-10-13 22:16:32 +02:00
Gigi
ec7371c43b fix: remove duplicate pane styles from index.css
- Removed 230+ lines of duplicate layout CSS
- Old inline styles were overriding our document scroll fixes
- Styles now only defined in src/styles/layout/app.css
- This fixes panes having overflow-y: auto and height: 100%
2025-10-13 22:14:57 +02:00
Gigi
35204ee400 fix: force document scroll with !important overrides
- Add explicit overflow: visible to main pane
- Add height: auto to main pane
- Ensure three-pane container doesn't constrain height
- Force styles to override any inherited overflow
2025-10-13 22:13:47 +02:00
Gigi
d1031b3342 fix: update Tailwind CSS import syntax for v4
- Change from @tailwind directives to @import syntax
- Move shimmer keyframes to CSS file (v4 convention)
- Fix Tailwind classes not being processed
2025-10-13 22:11:44 +02:00
Gigi
db67e94b9e fix: update PostCSS config for Tailwind v4
- Install @tailwindcss/postcss for Tailwind v4 compatibility
- Update postcss.config.js to use new plugin format
- Fix dev server PostCSS errors
2025-10-13 22:03:38 +02:00
Gigi
a0e5ba3a63 docs: add comprehensive Tailwind migration documentation
- Document completed migration phases (setup, base, layout, components)
- Track metrics: 190+ lines of CSS removed
- Define strategy for incremental component migrations
- Document z-index layering and responsive breakpoints
- Provide technical notes for future development
- Mark core migration as complete and production-ready
2025-10-13 22:00:34 +02:00
Gigi
f3f80449a6 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
2025-10-13 21:58:50 +02:00
Gigi
bd0b4e848f docs: update changelog with Tailwind migration progress 2025-10-13 21:38:10 +02:00
Gigi
4f5ba99214 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
2025-10-13 21:37:08 +02:00
Gigi
aab67d8375 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
2025-10-13 21:36:08 +02:00
Gigi
dbc0a48194 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)
2025-10-13 21:18:31 +02:00
Gigi
6a84646b0b 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
2025-10-13 21:17:11 +02:00
Gigi
e921967082 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
2025-10-13 21:04:39 +02:00
Gigi
ec34bc3d04 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
2025-10-13 21:02:52 +02:00
Gigi
96ce12b952 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
2025-10-13 21:01:44 +02:00
Gigi
1066c43d6c docs(changelog): add v0.5.7 entry with video features and improvements 2025-10-13 20:57:33 +02:00
Gigi
914557a61d chore: bump version to 0.5.7 2025-10-13 20:56:41 +02:00
Gigi
3df2f248ff fix: use negative margins to make video edge-to-edge within reader
- Add negative left/right margins (-0.75rem) to counteract reader padding
- Video now extends to the true edges of the reader container
- Maintains responsive sizing with 80vw width and aspect ratio
- Achieves true edge-to-edge video display
2025-10-13 20:53:39 +02:00
Gigi
d2770d58e2 fix: remove left/right margins from video player for edge-to-edge display
- Change margin from '0 auto 1rem auto' to '0 0 1rem 0'
- Remove auto left/right margins that were centering the video
- Keep bottom margin for spacing from content below
- Video player now aligns to left edge of card container
2025-10-13 20:27:08 +02:00
Gigi
933182567d fix: use viewport width for video container to break parent constraints
- Change width from 100% to 80vw (80% of viewport width)
- Increase min-width to 400px for better minimum size
- Increase max-width to 1000px for larger screens
- This makes video container independent of parent width constraints
- Ensures video is always properly sized regardless of title length
2025-10-13 20:23:12 +02:00
Gigi
f9fa2f05f0 feat: implement responsive video player with aspect ratio
- Update ReactPlayer to use width='100%', height='auto' with aspectRatio: '16/9'
- Replace padding-top approach with modern aspect-ratio CSS property
- Add minimum width (300px) and maximum width (800px) constraints
- Center video container with margin: 0 auto
- Ensure video player is no longer constrained by title length
- Improve video viewing experience across different screen sizes
2025-10-13 20:19:08 +02:00
Gigi
919bb8151f fix: resolve linting and type checking issues
- Replace 'any' type with proper UserSettings type in CompactView
- Fix import path for UserSettings from services/settingsService
- Resolve @typescript-eslint/no-explicit-any warning
- Ensure all TypeScript type checks pass
- Maintain strict linting rules without removing any rules
2025-10-13 20:14:54 +02:00
Gigi
6f82674c9b feat: add thumbnail images to compact view
- Add compact-thumbnail styling for small square images (24x24px)
- Update CompactView component to include thumbnail images on the left
- Use useImageCache hook to get cached article images
- Add settings prop to CompactView interface
- Position thumbnails before bookmark type icon in compact row
- Match design from screenshot with small square thumbnails on left side
- Improve visual hierarchy and content recognition in compact view
2025-10-13 20:12:54 +02:00
Gigi
8caf9988fc feat: enhance borders for reading list cards
- Add specific border styling for .bookmarks-list .individual-bookmark
- Use darker border color (#444) for better visibility
- Add background color (#1a1a1a) to make cards more distinct
- Enhance hover states with brighter border (#555) and background (#252525)
- Use !important to ensure styles override existing CSS
- Improves visual separation and card definition in reading list
2025-10-13 20:11:08 +02:00
Gigi
036ee20d98 feat: add URL routing for /me page tabs
- Add routes for /me/highlights, /me/reading-list, /me/archive
- Redirect /me to /me/highlights by default
- Update Bookmarks component to extract tab from URL path
- Pass activeTab prop to Me component based on current route
- Update Me component to use URL-based tab state instead of local state
- Update tab click handlers to navigate to appropriate URLs
- Enable deep-linking to specific tabs (e.g., /me/reading-list)
2025-10-13 20:09:46 +02:00
Gigi
b86545dcc8 fix: left-align text in reading list elements
- Add text-align: left to .bookmarks-list to override center alignment from .app
- Apply left alignment to all individual bookmark elements and their children
- Ensures reading list content is properly left-aligned for better readability
- Maintains consistent text alignment for bookmark titles, content, and metadata
2025-10-13 20:07:05 +02:00
Gigi
8bdccd9c9e feat: enable bookmark navigation in reading list
- Add useNavigate hook to Me component
- Implement handleSelectUrl function for bookmark navigation
- Pass onSelectUrl prop to BookmarkItem components in reading list
- Support both regular URLs (/r/*) and nostr articles (/a/*) navigation
- Enables clicking bookmarks in reading list to open content in main pane
2025-10-13 20:06:42 +02:00
Gigi
9a14185fa5 feat: color reading list tab blue to match bookmarks icon
- Apply #646cff color to reading-list tab when active
- Matches the blue color used throughout the app for bookmarks
- Provides visual consistency between bookmarks icon and reading list tab
- Uses same color as bookmark-type and other bookmark-related elements
2025-10-13 20:03:51 +02:00
Gigi
53a6053464 fix: prevent profile element from bleeding off screen on mobile
- Add horizontal margin (0 1rem) to author-card-container on mobile
- Set max-width to calc(100vw - 2rem) to ensure it fits within screen bounds
- Add box-sizing: border-box to both container and card for proper sizing
- Ensures profile element has equal left/right margins and doesn't exceed screen width
2025-10-13 20:03:00 +02:00
Gigi
e27d7ee26c feat: increase spacing between mobile buttons and profile element
- Increase margin-top from 2.25rem to 3.5rem for explore-header on mobile
- Provides more breathing room between floating action buttons and profile
- Improves mobile UX by preventing visual crowding
2025-10-13 20:01:22 +02:00
Gigi
98203e6b6f feat: hide tab counts on mobile for /me page
- Wrap tab labels and counts in separate spans for better control
- Hide counts on mobile devices (max-width: 768px) to save space
- Maintain counts on desktop for better UX
- Follows mobile-first design principles
2025-10-13 19:59:16 +02:00
Gigi
8469740141 fix: resolve TypeScript errors in youtube-meta.ts
- Remove non-existent getVideoDetails import and usage
- Fix getSubtitles API call to match actual package interface
- Add proper Subtitle type to replace any usage
- Convert subtitle data types to match Caption interface
- Install missing @vercel/node dependency
2025-10-13 19:57:38 +02:00
Gigi
8fff2bce52 feat(api): add Vimeo video metadata extraction support
- Create unified video-meta.ts API handler for both YouTube and Vimeo
- Add Vimeo oEmbed API integration for server-side metadata extraction
- Implement URL pattern matching for YouTube and Vimeo video detection
- Support both URL and videoId parameters for backward compatibility
- Add proper TypeScript types for Vimeo oEmbed response
- Include caching mechanism for Vimeo metadata (7-day cache)
- Remove unused @vimeo/player package dependency

The new API endpoint supports:
- YouTube: /api/video-meta?url=https://youtube.com/watch?v=ID or ?videoId=ID
- Vimeo: /api/video-meta?url=https://vimeo.com/ID
- Returns consistent response format for both platforms
2025-10-13 19:52:01 +02:00
Gigi
30b98fc744 refactor(api): improve type safety in youtube-meta handler
- Replace 'any' type with proper type annotations
- Add explicit type checking for video details response
- Improve description field extraction with better type safety
- Add comments for better code documentation
2025-10-13 19:49:04 +02:00
Gigi
7a190b7d35 fix(api): be more lenient extracting YouTube description from details fields 2025-10-13 19:43:04 +02:00
Gigi
e3149c40c7 fix(types): correct setTimeout ref type in Settings to ReturnType<typeof setTimeout> 2025-10-13 19:37:37 +02:00
Gigi
91743518bd feat(reader): use YouTube title as header title, description as body; show transcript section 2025-10-13 19:33:42 +02:00
Gigi
fd2e4079ab feat(reader): fetch YouTube title/description/captions with 7d client cache; transcript toggle 2025-10-13 19:28:46 +02:00
Gigi
ec423cad80 feat(services): youtubeMetaService with 7d localStorage cache and ID extraction 2025-10-13 19:27:34 +02:00
Gigi
8f8441b0e0 feat(api): youtube-meta endpoint with 7d in-memory cache and captions/details via extractor 2025-10-13 19:26:53 +02:00
Gigi
3c20d45dba chore(deps): add @treeee/youtube-caption-extractor 2025-10-13 19:25:59 +02:00
Gigi
75c4e20dc9 fix(video): implement proper react-player responsive pattern from docs 2025-10-13 19:12:59 +02:00
Gigi
9d27595d31 fix(video): simplify video container - remove negative margins and complex layout hacks 2025-10-13 19:10:56 +02:00
Gigi
b7d90a790b style(layout): remove max-width on main pane, constrain reader instead; full width for videos 2025-10-13 19:08:57 +02:00
Gigi
c49d850f74 refactor(video): extract buildNativeVideoUrl to reusable utility (DRY) 2025-10-13 18:31:29 +02:00
Gigi
4c11c5fc54 fix(reader): use responsive aspect-ratio container for videos to fill full width 2025-10-13 18:30:09 +02:00
Gigi
44befab6d3 style(reader): make video container break out of reader padding for full width 2025-10-13 18:28:28 +02:00
Gigi
02a2f4b85e chore(deps): update package-lock.json for version 0.5.6 and react-player 2025-10-13 18:27:43 +02:00
Gigi
43d54b5734 refactor(bookmarks): clean up unused getIconForUrlType in CompactView and fix prop passing 2025-10-13 18:27:22 +02:00
Gigi
b7896be507 fix(types): replace ShareData with inline type to fix lint errors 2025-10-13 18:26:36 +02:00
Gigi
eeb40306da style(layout): make main pane full width when displaying videos 2025-10-13 18:25:08 +02:00
Gigi
749b47ac5c feat(reader): show 'Mark as Watched' for video URLs (icon unchanged) 2025-10-13 18:24:18 +02:00
Gigi
42f59f2b19 feat(reader): add three-dot menu under videos with open/native/copy/share actions 2025-10-13 18:23:12 +02:00
Gigi
2bf6e742f1 feat(reader): show video duration for /r/ video URLs using react-player onDuration 2025-10-13 17:30:36 +02:00
Gigi
2a2049e678 style(reader): widen main pane when showing videos; add reader-video styles 2025-10-13 17:29:21 +02:00
Gigi
146aa85e76 feat(bookmarks): make entire bookmark cards clickable; stop propagation on internal controls 2025-10-13 17:27:25 +02:00
Gigi
a26c7497b5 feat(reader): embed external videos in /r/ using react-player; add vimeo/dailymotion detection 2025-10-13 17:25:34 +02:00
Gigi
da67135f5e chore(deps): add react-player for embedding videos in reader 2025-10-13 17:24:10 +02:00
Gigi
aebb6d1762 refactor(bookmarks): remove READ/VIEW/WATCH CTA buttons and texts; simplify classifyUrl 2025-10-13 17:21:59 +02:00
Gigi
8f5cf6a0b4 refactor(utils): remove CTA buttonText from classifyUrl and UrlClassification 2025-10-13 17:20:02 +02:00
Gigi
875017db96 docs(changelog): add v0.5.6 entry (Keep a Changelog format) 2025-10-13 17:15:34 +02:00
Gigi
c0f34b684d chore: bump version to 0.5.6 2025-10-13 17:12:24 +02:00
Gigi
613956bbaf fix: use round checkmark icon (faCheckCircle) in Mark as Read button 2025-10-13 17:10:29 +02:00
Gigi
041ba5c05b style: remove horizontal divider above Mark as Read button 2025-10-13 17:09:26 +02:00
Gigi
05c21cfd6d style: remove horizontal divider below article menu button 2025-10-13 17:07:02 +02:00
Gigi
4898f99ae1 style: make article menu button more subtle by removing border 2025-10-13 17:05:49 +02:00
Gigi
be920e8c44 fix: remove extra horizontal divider above article menu 2025-10-13 17:05:15 +02:00
Gigi
0fa5ac536b feat: add three-dot menu to articles and enhance highlight menus
- Add three-dot menu button at end of articles (before Mark as Read)
- Right-aligned menu with two options:
  - Open on Nostr (using nostr gateway/portal)
  - Open with Native App (using nostr: URI scheme)
- Add 'Open with Native App' option to highlight card menus
- Menu only appears for nostr-native articles (kind:30023)
- Styled consistently with highlight card menus
- Click outside to close menu functionality
2025-10-13 17:03:00 +02:00
Gigi
cef359af29 fix: ensure code blocks use monospace fonts explicitly
- Add explicit monospace font-family to all pre elements
- Include Courier New as additional cross-platform fallback
- Apply to both reader-markdown and reader-html contexts
- Ensures code always renders in monospace even if Prism theme is overridden
2025-10-13 16:58:03 +02:00
Gigi
2de72b73c1 feat: add Prism.js syntax highlighting for code blocks
- Install prismjs and rehype-prism-plus packages
- Integrate rehype-prism plugin into ReactMarkdown
- Use prism-tomorrow dark theme for syntax highlighting
- Enhanced code block styling with better padding and borders
- Inline code now has distinct styling from code blocks
- Monospace font for all code (Monaco, Menlo, Consolas)
- Improved readability with proper line-height and spacing
2025-10-13 16:55:06 +02:00
Gigi
a794331c1a feat: add image placeholders to blog post cards in /explore
- Show newspaper icon placeholder when blog posts don't have images
- Always render image container with consistent height
- Match the same placeholder style as large bookmark preview
- Improves visual consistency across the app
2025-10-13 16:52:32 +02:00
Gigi
e09be543bc feat: add caching to /me page for faster loading
- Create meCache service to store highlights, bookmarks, and read articles
- Seed Me component from cache on load to avoid empty flash
- Show small spinner while refreshing if cached data is displayed
- Update cache when highlights are deleted
- Only show full loading screen if no cached data is available
- Improves perceived performance similar to /explore page
2025-10-13 16:50:43 +02:00
Gigi
88085c48d2 style: improve bookmarks sidebar visual design
- Replace green buttons with purple/blue primary color
- Add subtle borders to card and large preview views
- Enable image previews in card view for all bookmarks (not just articles)
- Fetch OG images for regular bookmarks in card view
- Improve hover states with lighter border colors
2025-10-13 16:47:41 +02:00
Gigi
e32010771b refactor: make /me Reading List use same components as bookmark sidebar
- Reuse BookmarkItem component for rendering individual bookmarks
- Apply same filtering logic (hasContentOrUrl) as BookmarkList
- Add view mode controls (compact/cards/large) matching sidebar
- Count shows individual bookmarks not bookmark lists
- Keeps code DRY by reusing existing components and logic
2025-10-13 16:41:01 +02:00
Gigi
03e7484e71 fix: preserve reading font settings in markdown images
- Remove inline styles from custom image component
- Let CSS inheritance handle font and styling properly
- Images now respect user's reading font and size settings
2025-10-13 16:35:11 +02:00
Gigi
d9fd4ec286 feat: enable inline image rendering in nostr-native blog posts
- Install rehype-raw plugin for HTML support in ReactMarkdown
- Configure ReactMarkdown to parse and render HTML img tags
- Add responsive image styling with max-width and auto height
- Images now render inline in nostr-native blog posts with proper styling
2025-10-13 16:34:08 +02:00
Gigi
8f14f0347c docs: update CHANGELOG for v0.5.5 2025-10-13 16:33:03 +02:00
Gigi
9b5bb8496f chore: bump version to 0.5.5 2025-10-13 16:32:25 +02:00
Gigi
9264a78c95 Merge pull request #4 from dergigi/me-page
feat(me): two-pane layout, tabs, refined highlight cards, and mark-as-read
2025-10-13 15:31:44 +01:00
Gigi
326d571871 fix(content): include currentArticle in useEffect deps to satisfy lint 2025-10-13 16:27:18 +02:00
Gigi
744e86b290 style: match /me profile card width to highlight cards
- Constrain AuthorCard width via header context
- Remove extra padding on .me-highlights-list so widths align
- Keeps both at 600px max with auto centering
2025-10-13 16:20:54 +02:00
Gigi
e46b68da7e style: improve Me page mobile tabs and avoid overlap with sidebar buttons
- Add top margin to header on mobile for floating buttons
- Tighten tab paddings and content spacing
- Reduce left/right padding for more room on small screens
2025-10-13 16:15:21 +02:00
Gigi
811a962632 style: reduce margins/paddings to make highlight cards more compact
- Decrease list gap and content padding
- Bring corner buttons closer to edges (header/footer 0.5rem, quote 0.25/0.5rem)
- Keep safe spacing to avoid text/button overlap
2025-10-13 16:13:11 +02:00
Gigi
eb82e8762a style: tighten vertical spacing on highlight cards
- Reduce header/footer vertical padding (0.5rem -> 0.375rem)
- Reduce content top/bottom padding (3rem -> 2.25rem)
- Slightly reduce gaps to compact layout
2025-10-13 16:09:42 +02:00
Gigi
d919da153f feat: make quote icon a CompactButton in top-left corner
- Replace static quote icon with CompactButton
- Position top-left with same corner margin as others
- Preserve level-based coloring via CSS (
  - mine: var(--highlight-color-mine)
  - friends: var(--highlight-color-friends)
  - nostrverse: var(--highlight-color-nostrverse)
)
2025-10-13 16:08:37 +02:00
Gigi
8389d5811a fix: make relay button match menu spacing within footer
- Force footer relay indicator to be in normal flow (position: static)
- Remove margins and rely on footer gap/padding
- Ensures same visual spacing as the three-dot CompactButton
2025-10-13 15:55:18 +02:00
Gigi
0aa0c44441 fix: group relay icon and author in footer-left for consistent alignment
- Add  container (relay + author)
- Footer now uses space-between with left group and right menu
- Consistent gap and truncation behavior for author
- Matches the visual rhythm of the three-dot button
2025-10-13 15:47:22 +02:00
Gigi
49ea7504a1 style: make relay indicator match CompactButton (same look as menu)
- Remove custom opacity for relay indicator
- Rely on  styles for hover/active/spacing
- Ensures relay button visually matches three-dot menu button
2025-10-13 15:37:16 +02:00
Gigi
6602fb9359 fix: align relay indicator within footer with symmetric spacing
- Place relay indicator as first element in footer (no absolute positioning)
- Remove extra author left padding; rely on footer gap/padding
- Ensure consistent 1rem outer padding and 0.75rem gap between footer items
- Matches spacing of timestamp and menu in their corners
2025-10-13 15:35:07 +02:00
Gigi
731eb6915a fix: align corner elements symmetrically with proper margins
- Change quote icon left margin from 0.5rem to 1rem
- Change relay indicator left margin from 0.5rem to 1rem
- All corner elements now have 1rem horizontal margin (matching header/footer padding)
- Adjust author padding-left to 2.5rem to accommodate relay icon
- Creates symmetrical appearance across all four corners
2025-10-13 15:28:55 +02:00
Gigi
3459179310 refactor: move quote icon to top-left corner and make it smaller
- Position quote icon absolutely in top-left corner (0.5rem, 0.5rem)
- Reduce font size from 1.2rem to 0.85rem
- Add opacity: 0.7 to make it more subtle
- Remove quote icon from document flow
- Update content padding to be uniform (3rem 1rem)
- Remove gap from highlight-item since content is only in-flow element
- Clean up mobile styles for quote icon
2025-10-13 15:25:10 +02:00
Gigi
b1f951daf5 fix: position relay indicator in bottom-left corner
- Move relay indicator to be absolutely positioned in bottom-left corner
- Similar to timestamp in top-right corner
- Add padding-left to author name to prevent overlap with relay icon
- Relay indicator sits outside the footer content flow
- z-index: 10 ensures it's above footer background
2025-10-13 15:23:29 +02:00
Gigi
caebcec0af fix: move relay indicator to footer to prevent overlap with author
- Move relay indicator from quote icon to footer as first element
- Remove absolute positioning from relay indicator
- Update footer layout: relay icon, author name, menu button (left to right)
- Author name now sits to the right of relay icon with no overlap
- Use margin-right: auto on author to push menu to the right
2025-10-13 15:20:40 +02:00
Gigi
5f50f4b8d6 fix: improve spacing and alignment of highlight card elements
- Remove padding from highlight-item, let header/footer/content handle it
- Fix header and footer to use left: 0 and right: 0 instead of negative offsets
- Use padding on quote-icon and highlight-content for proper spacing
- Adjust relay indicator positioning to work with new padding model
- Ensure all elements have proper vertical and horizontal spacing
2025-10-13 15:17:08 +02:00
Gigi
3039208ba0 refactor: make header and footer full-width with borders and corners
- Header and footer now span 100% width of the card
- Header has top border and top rounded corners
- Footer has bottom border and bottom rounded corners
- Main item has only left/right borders
- Properly adjust padding to accommodate absolute positioned header/footer
- Border colors transition correctly for hover, selected, and level states
2025-10-13 15:03:53 +02:00
Gigi
397c956e87 refactor: create highlight-header for timestamp positioning
- Add highlight-header container for timestamp
- Position header absolutely in top-right corner
- Remove padding-right workaround from highlight-content
- Use pointer-events to allow timestamp click while preventing header clicks
- Cleaner structure prevents any text overlap naturally
2025-10-13 14:45:21 +02:00
Gigi
cf47ceb74b refactor: create highlight-footer for perfect alignment
- Rename highlight-meta to highlight-footer for semantic clarity
- Use flexbox with space-between for proper element spacing
- Ensure author name and menu button are perfectly vertically aligned
- Add min-height to author to match button height
2025-10-13 14:42:23 +02:00
Gigi
da7aa2c115 fix: add padding to prevent quote text from overlapping timestamp 2025-10-13 14:40:30 +02:00
Gigi
c0046bc04c refactor: remove clock icon from timestamp button 2025-10-13 14:38:31 +02:00
Gigi
2f8f6a0652 feat: introduce CompactButton component for highlight cards
- Create reusable CompactButton component for small, borderless buttons
- Refactor relay indicator to use CompactButton
- Refactor menu toggle button to use CompactButton
- Make timestamp clickable with CompactButton (shows full date on hover)
- Simplify CSS by removing duplicate button styles
- Improve mobile touch targets for all compact buttons
2025-10-13 14:01:51 +02:00
Gigi
9a6f788b98 feat: move highlight timestamp to top-right corner of cards 2025-10-13 12:55:00 +02:00
Gigi
c1a628260c feat(icons): import books.svg as raw and register faBooks; add *.svg?raw types 2025-10-13 12:42:56 +02:00
Gigi
7b0bd7077c feat: use faBooks icon for Mark as Read button 2025-10-13 12:29:23 +02:00
Gigi
7d47f0a86e feat: add custom FontAwesome Pro books icon for Archive tab 2025-10-13 12:28:08 +02:00
Gigi
44fcd74cbe refactor: rename Library tab to Archive 2025-10-13 12:20:18 +02:00
Gigi
5ac0e7ed87 fix: deduplicate article events in library to prevent showing duplicates 2025-10-13 12:15:16 +02:00
Gigi
743968f7fb feat: use user's custom highlight color for Highlights tab 2025-10-13 12:14:25 +02:00
Gigi
e1a3ae4b4d feat: render library articles using BlogPostCard component for consistency 2025-10-13 12:13:12 +02:00
Gigi
acf13448ae style: improve tab border styling for dark theme 2025-10-13 12:10:19 +02:00
Gigi
a5daa8b56c fix: remove incorrect useSettings hook usage in Me component 2025-10-13 12:02:15 +02:00
Gigi
267169c5c1 fix: correct fetchBookmarks usage with callback pattern in Me component 2025-10-13 10:45:46 +02:00
Gigi
89272dd9a3 style: left-align text inside author card 2025-10-13 10:37:16 +02:00
Gigi
d059212238 style: constrain /me page content width to match author card (600px) 2025-10-13 10:36:36 +02:00
Gigi
0d8a3576a6 feat: add tabbed layout to /me page with Highlights, Reading List, and Library tabs 2025-10-13 10:35:14 +02:00
Gigi
8910c2750a feat: replace username with AuthorCard component on /me page 2025-10-13 10:30:58 +02:00
Gigi
12393d6df4 feat: implement two-pane layout for /me page with article sources and highlights 2025-10-13 10:27:18 +02:00
Gigi
6c0a2439ad feat: instant mark-as-read with checkmark animation and read status checking
- Add functions to check if article/URL was already marked as read via NIP-25 reactions
- Make mark-as-read action instant with fire-and-forget publishing
- Add checkmark icon animation when marking as read
- Display read status on load by querying kind:7 (nostr events) and kind:17 (websites) reactions
- Add green styling for already-read state
- Button shows checkmark and is disabled when article is already marked as read
2025-10-13 10:17:16 +02:00
Gigi
d83712127b docs: update CHANGELOG for v0.5.4 2025-10-13 10:08:32 +02:00
Gigi
55325cd7ad chore(release): bump version to 0.5.4 2025-10-13 10:05:53 +02:00
Gigi
82e508fca6 refactor(styles): extract base, layout, components, utils CSS and use aggregator imports in src/index.css 2025-10-13 09:47:09 +02:00
Gigi
8ff32e9363 chore(styles): scaffold styles directory and empty files 2025-10-13 09:40:23 +02:00
Gigi
477308632b fix: add safe area insets for symmetrical mobile button positioning
- Add safe-area-inset-top/bottom/left/right to all mobile floating buttons
- Ensures proper spacing on devices with notches and home indicators
- Makes relay status indicator perfectly aligned with bookmark/highlights buttons
2025-10-13 09:23:17 +02:00
Gigi
9ffd06f5e3 docs: update CHANGELOG for v0.5.3 2025-10-13 08:55:59 +02:00
Gigi
a89c87819a chore: bump version to 0.5.3 2025-10-13 08:55:12 +02:00
Gigi
b09ae3bae3 fix: increase profile icon size when logged out
- Change .profile-avatar svg font-size from 1rem to 1.25rem
- Matches size of other icon buttons in sidebar header
2025-10-13 08:54:18 +02:00
Gigi
6ea8c0d40e feat: make relay status indicator smaller and hide on scroll
- Reduce overall size of indicator on desktop (smaller padding, font sizes)
- On mobile, match size with sidebar toggle buttons (var(--min-touch-target))
- Auto-collapse on mobile (collapsed by default, tap to expand)
- Hide when scrolling down on mobile, show when scrolling up
- Match behavior of other mobile UI controls for consistency
2025-10-13 08:53:17 +02:00
Gigi
079501337c fix: filter out invalid bookmarks without IDs
- Skip bookmarks that don't have a valid ID instead of generating temporary IDs
- Use bookmark's original created_at timestamp for added_at instead of Date.now()
- Fixes issue where first 2 bookmarks always showed 'Now' timestamp with no content
2025-10-13 08:50:29 +02:00
Gigi
5bf0382227 docs(changelog): add v0.5.1 and v0.5.2 entries 2025-10-13 00:14:16 +02:00
Gigi
0199c59014 chore: bump version to 0.5.2 2025-10-13 00:12:51 +02:00
Gigi
44fb63fc59 fix: resolve linting errors in HighlightItem
- Remove unused handleDeleteClick function (replaced by handleMenuDeleteClick)
- Remove onSelectUrl from destructuring (not used after menu refactor)
- Add comment explaining onSelectUrl is kept in props for API compatibility
2025-10-13 00:12:25 +02:00
Gigi
13a28d2dbd fix: make getNostrUrl detect identifier type for ants.sh
- ants.sh requires /p/ for profiles (npub/nprofile) and /e/ for events (note/nevent/naddr)
- Updated getNostrUrl to automatically detect identifier type and use correct path
2025-10-13 00:07:46 +02:00
Gigi
f87a7da32e feat: switch Nostr gateway to ants.sh
- Replace njump.me and search.dergigi.com with ants.sh
- Use ants.sh/p/ for profiles and ants.sh/e/ for events
- All existing helper functions continue to work with new gateway
2025-10-13 00:05:44 +02:00
Gigi
8fdf9938c2 refactor: centralize Nostr gateway URLs in config
- Create nostrGateways.ts config file with PRIMARY (njump.me) and SEARCH (search.dergigi.com) gateways
- Add helper functions: getProfileUrl, getEventUrl, getNostrUrl
- Update all hardcoded gateway URLs across the codebase to use the config
- Updated files: HighlightItem, nostrUriResolver, BookmarkViews (Card/Large), ResolvedMention
2025-10-13 00:05:11 +02:00
Gigi
ee4d480961 refactor: remove 'Loading your highlights' text from Me page 2025-10-13 00:01:41 +02:00
Gigi
bd866549a0 fix: open on nostr now opens the highlight event itself
- Changed 'Open on Nostr' to link to the highlight event (kind 9802)
- Previously it was linking to the article being highlighted
- Menu item is now always shown since every highlight has an event ID
- Removed unused handleLinkClick function
2025-10-13 00:01:11 +02:00
Gigi
7c39f1d821 style: change three-dot menu icon to horizontal 2025-10-13 00:00:05 +02:00
Gigi
e6a7bb4c98 feat: add three-dot menu to highlight cards
- Replace external link button with three-dot menu in highlight cards
- Move 'Open source/Nostr' and 'Delete' actions into dropdown menu
- Add click-outside functionality to close menu
- Style menu for both dark and light themes
2025-10-12 23:59:28 +02:00
Gigi
14cf3189b8 chore: remove 'Refreshing posts…' text from Explore
Only show spinner icon without text for cleaner UI
2025-10-12 23:56:23 +02:00
Gigi
66b060627a chore: bump version to 0.5.1 2025-10-12 23:54:17 +02:00
Gigi
d9bcf14baa fix: match highlight count indicator style to reading-time element
- Use rgba() with 0.1 opacity for background (subtle, not bright)
- Use rgba() with 0.3 opacity for border
- Set text and icon color to white for better visibility
- Properly converts hex colors to rgba using hexToRgb helper
2025-10-12 23:53:10 +02:00
Gigi
c571e6ebf7 fix: reduce brightness and add colored border to highlight count indicator
- Set background color to 75% opacity (bf hex) to reduce brightness
- Add border color matching the highlight group color
- Makes the indicator less overwhelming while maintaining visibility
2025-10-12 23:51:05 +02:00
Gigi
fb06a1aec3 fix: apply user highlight color to both marker and arrow icons
- Apply color style directly to both icons in collapsed highlights button
- Update CSS to use color-agnostic pulse animation (opacity/scale)
- Remove hardcoded yellow color and drop-shadow from glow effect
2025-10-12 23:50:34 +02:00
Gigi
5a0d08641b chore: remove MOBILE_IMPLEMENTATION.md
No longer needed as mobile features are now integrated
2025-10-12 23:49:12 +02:00
Gigi
8a8419385e fix: apply highlight group color to background of count indicator
- Change highlight count indicator to use backgroundColor instead of color
- Set text color to black for contrast against colored backgrounds
- Maintains priority: nostrverse > friends > mine
2025-10-12 23:48:56 +02:00
Gigi
0d5dc6e785 feat: apply 'my highlights' color to collapsed highlights button
- Update HighlightsPanelCollapsed to accept settings prop
- Apply highlightColorMine to the expand button icon and text
- Pass settings through HighlightsPanel and ThreePaneLayout
2025-10-12 23:48:18 +02:00
Gigi
1d90333803 feat: apply highlight group color to highlight count indicator
- Update ReaderHeader to receive highlights and highlightVisibility props
- Calculate dominant color based on visible highlight groups
- Apply color priority: nostrverse > friends > mine
- Highlight count indicator now reflects the active group colors
2025-10-12 23:47:05 +02:00
Gigi
91e6e62688 feat: apply 'my highlights' color to highlight buttons
- Update floating highlight button (FAB) to use highlightColorMine from settings
- Update mobile highlights button to use highlightColorMine from settings
- Both buttons now reflect the user's chosen 'my highlights' color
2025-10-12 23:44:51 +02:00
Gigi
619a8a9753 docs(changelog): add v0.5.0 changes and compare links 2025-10-12 23:42:44 +02:00
Gigi
0fe38e94d3 chore: bump version to 0.5.0 2025-10-12 23:38:51 +02:00
Gigi
722e8adbdf Merge pull request #3 from dergigi/local-first
perf: parallel local-first fetching, instant render, and non-wiping refreshes
2025-10-12 22:37:38 +01:00
Gigi
886d5ac08c chore(lint): satisfy react-hooks dependency in Explore; lints clean 2025-10-12 23:35:23 +02:00
Gigi
89d5ba4c37 ui(explore): shrink refresh spinner footprint; inline-sized loading row 2025-10-12 23:33:28 +02:00
Gigi
b8b9f82d91 ux(explore): preserve posts across navigations; seed from cache; merge streamed + final 2025-10-12 23:30:56 +02:00
Gigi
b3fc9bb5c3 ux(bookmarks): avoid clearing list when no new events; decouple refetch from route changes 2025-10-12 23:26:18 +02:00
Gigi
d2ebcd8fbe ux(explore): keep posts visible during refresh; inline spinner; no list wipe 2025-10-12 23:25:05 +02:00
Gigi
68c9623c35 ux(bookmarks): keep list visible during refresh; show spinner only; no wipe 2025-10-12 23:22:53 +02:00
Gigi
496d1df404 perf(parallel): run local+remote fetches concurrently across services; stream+dedupe 2025-10-12 23:13:26 +02:00
Gigi
ea1046fe13 perf(local-first): always follow up with remote for articles and titles 2025-10-12 23:00:23 +02:00
Gigi
6d58d6e7f3 fix(highlights): ensure nostrverse fetch merges remote results after local for article/url 2025-10-12 22:58:25 +02:00
Gigi
e1420140d1 chore(lint): fix eslint and typescript issues; no rule changes 2025-10-12 22:55:46 +02:00
Gigi
484c2e0c2f refactor(highlights): split service into smaller modules; keep files <210 lines 2025-10-12 22:54:11 +02:00
Gigi
31f7d53829 perf(explore): stream contacts + early posts from local; merge remote later 2025-10-12 22:43:35 +02:00
Gigi
e3debfa5df perf(local-first): apply local-first then remote pattern across services (titles, bookmarks, highlights) 2025-10-12 22:42:24 +02:00
Gigi
a1305fba81 fix(explore): always query remote relays after local; stream merge into UI 2025-10-12 22:38:29 +02:00
Gigi
ca95d6c7f4 perf(ui): stream results to UI; show cached/local immediately (articles, highlights, explore) 2025-10-12 22:33:46 +02:00
Gigi
5513fc9850 perf(relays): local-first queries with short timeouts; fallback to remote if needed 2025-10-12 22:06:49 +02:00
Gigi
86de98e644 Merge pull request #2 from dergigi/pwa
Upgrade to full Progressive Web App
2025-10-12 07:54:37 +01:00
Gigi
fd374cd705 docs: remove temporary PWA launch checklist and implementation summary 2025-10-12 07:18:20 +01:00
Gigi
20b4658bef docs(pwa): update implementation summary to reflect final icons and next steps 2025-10-12 07:18:02 +01:00
Gigi
0850ba250c chore(lint): fix service worker typings and satisfy ESLint for worker globals 2025-10-12 07:15:52 +01:00
Gigi
b71d188fd8 chore: remove favicon zip file after extraction 2025-10-11 21:01:27 +01:00
Gigi
579f6b9a96 docs: update PWA docs to reflect branded icons are now in place 2025-10-11 20:58:56 +01:00
Gigi
d9403a73c6 feat(icons): replace placeholder icons with branded favicons
- Replace placeholder PWA icons with proper Boris-branded icons
- Add favicon.ico, favicon-16x16.png, favicon-32x32.png
- Add apple-touch-icon.png for iOS devices
- Update index.html with proper favicon links
- All icons extracted from boris-favicon.zip
- PWA icons now use android-chrome-192x192 and android-chrome-512x512
2025-10-11 20:58:26 +01:00
Gigi
747811fa94 docs: add PWA launch checklist 2025-10-11 20:43:13 +01:00
Gigi
489e480394 docs: add PWA implementation summary and guide 2025-10-11 20:42:29 +01:00
Gigi
418bcb0295 feat(pwa): upgrade to full PWA with vite-plugin-pwa
- Add web app manifest with proper metadata and icon support
- Configure vite-plugin-pwa with injectManifest strategy
- Migrate service worker to Workbox with precaching and runtime caching
- Add runtime caching for cross-origin images (preserves existing behavior)
- Add runtime caching for cross-origin article HTML for offline reading
- Create PWA install hook and UI component in settings
- Add online/offline status monitoring and toast notifications
- Add service worker update notifications
- Add placeholder PWA icons (192x192, 512x512, maskable variants)
- Update HTML with manifest link and theme-color meta tag
- Preserve existing relay/airplane mode functionality (WebSockets not intercepted)

The app now passes PWA installability criteria while maintaining all existing
offline functionality. Icons should be replaced with proper branded designs.
2025-10-11 20:41:49 +01:00
Gigi
88f01554e7 fix: improve mobile touch targets for highlight icons
- Increase touch target size to 44x44px on mobile (relay & delete icons)
- Add proper padding to both icons for larger tap area
- Increase spacing between relay and delete icons
- Expand quote icon container width on mobile to accommodate both targets
- Increase icon font size on mobile (0.85rem) for better visibility
- Move icons slightly outward (-8px) to prevent overlap

Icons are now much easier to tap on mobile devices without
accidentally hitting the wrong button.
2025-10-11 09:06:17 +01:00
Gigi
c85092a644 fix: color /me highlights with 'my highlights' color setting
- Set level: 'mine' on all highlights in /me page
- Highlights now use the customizable --highlight-color-mine CSS variable
- Quote icons and borders automatically match user's color preference
- Consistent styling with highlights shown elsewhere in the app
2025-10-11 09:04:48 +01:00
Gigi
096478bcec feat: add author info card for nostr-native articles
- Create AuthorCard component showing profile picture and bio
- Display author card after mark as read button
- Only shown for nostr-native articles (not external URLs)
- Fetch author profile data using applesauce ProfileModel
- Card displays author name, avatar, and bio (truncated to 3 lines)
- Responsive design with smaller avatar on mobile
- Elegant card styling matching app design system

Author information helps readers learn more about article authors
directly within the reading experience.
2025-10-11 08:55:37 +01:00
Gigi
b8de4a85e0 docs: update CHANGELOG for v0.4.3 release 2025-10-11 08:40:05 +01:00
Gigi
a5b7cedfaa chore: bump version to 0.4.3 2025-10-11 08:39:16 +01:00
Gigi
0adb8d6766 feat: add highlight deletion with confirmation dialog
- Create deletionService for NIP-09 kind:5 event deletion requests
- Add ConfirmDialog component for user confirmation before deletion
- Add subtle delete button to highlight items (trash icon)
- Only show delete button for user's own highlights
- Position delete button symmetrically opposite to relay indicator
- Add confirmation dialog to prevent accidental deletions
- Remove highlights from UI immediately after deletion request
- Style delete button with red hover color
- Add comprehensive confirmation dialog styling (danger/warning/info variants)

Implements NIP-09 Event Deletion Request.
Users can now delete their own highlights after confirming the action.
2025-10-11 08:38:22 +01:00
Gigi
6a6b8c4fad feat: add mark as read button for articles
- Create reactionService for handling kind:7 and kind:17 reactions
- Add mark as read button at the end of articles (📚 emoji)
- Use kind:7 reaction for nostr-native articles (/a/ paths)
- Use kind:17 reaction for external websites (/r/ paths)
- Pass activeAccount and currentArticle props through component tree
- Add responsive styling for mark as read button
- Button shows loading state while creating reaction
- Only visible when user is logged in

Implements NIP-25 (kind:7 reactions) and NIP-25 (kind:17 website reactions).
Users can now mark articles as read, creating a permanent record on nostr.
2025-10-11 08:34:36 +01:00
Gigi
4f952816ea feat: compact relay status indicator on mobile
- Show only icon on mobile (44x44px touch target)
- Tap to expand for full details
- Smooth transition between compact and expanded states
- Maintains full display on desktop
- Reduces screen clutter on mobile while keeping info accessible

Flight mode notice now just shows airplane icon on mobile.
Tap it to see full connection details.
2025-10-11 08:09:29 +01:00
Gigi
76835e2509 feat: add /me page to show user's recent highlights
- Create Me component to display logged-in user's highlights
- Add /me route to App routing
- Update SidebarHeader to navigate to /me when clicking profile avatar
- Integrate Me page in ThreePaneLayout (same as Settings/Explore)
- Show user profile info and highlight count
- List all highlights created by the user

Clicking the profile picture now takes you to your personal highlights page.
2025-10-11 08:07:20 +01:00
Gigi
63af770c83 docs: update CHANGELOG for v0.4.2 release 2025-10-11 03:25:11 +01:00
Gigi
165c427e5f chore: bump version to 0.4.2 2025-10-11 03:24:15 +01:00
Gigi
a0e30aa197 fix: resolve all linting and type errors
- Remove unused React import from nostrUriResolver
- Add block scoping to switch case statements
- Add react-hooks plugin to eslint config
- Fix exhaustive-deps warnings in components
- Fix DecodeResult type to use ReturnType<typeof decode>
- Update dependency arrays to include all used values
- Add eslint-disable comment for intentional dependency omission

All linting warnings resolved. TypeScript type checking passes.
2025-10-11 03:23:38 +01:00
Gigi
3a8203d26e fix: mobile button scroll detection on main pane element
- Update useScrollDirection to accept elementRef parameter
- Detect scroll on main pane div instead of window
- Create mainPaneRef and attach to scrollable content area
- Fix issue where scroll events weren't detected on mobile

On mobile, content scrolls within .pane.main (overflow-y: auto) not on window.
Now buttons properly hide on scroll down and show on scroll up.
2025-10-11 01:48:45 +01:00
Gigi
ffe848883e feat: resolve and display article titles for naddr references
- Add articleTitleResolver service to fetch article titles from relays
- Extract naddr identifiers from markdown content
- Fetch article titles in parallel using relay pool
- Replace naddr references with actual article titles
- Fallback to identifier if title fetch fails
- Update markdown processing to be async for title resolution
- Pass relayPool through component tree to enable resolution

Example: nostr:naddr1... now shows as "My Article Title" instead of "article:identifier"

Improves readability by showing human-friendly article titles in cross-references
2025-10-11 01:47:11 +01:00
Gigi
078a13c45d fix: link naddr articles internally instead of to njump
- Articles (naddr) now link to /a/{naddr} route (internal)
- Other nostr identifiers still link to njump.me (external)
- Improved article labels to show identifier instead of generic text
- Better UX: clicking article references opens them in-app
2025-10-11 01:43:54 +01:00
Gigi
8a69d5bc6b feat: resolve NIP-19 identifiers in article content
- Add nostrUriResolver utility to detect and replace nostr: URIs
- Support npub, note, nprofile, nevent, and naddr identifiers
- Convert nostr: URIs to clickable njump.me links
- Process markdown before rendering to handle nostr mentions
- Add CSS styling for nostr-uri-link class
- Implements NIP-19 and NIP-27 (nostr: URI scheme)

Nostr-native articles can now contain references like:
- nostr:npub1... → @npub1abc...
- nostr:note1... → note:note1abc...
- nostr:naddr1... → article:identifier

All identifiers become clickable links to njump.me
2025-10-11 01:42:03 +01:00
Gigi
6783ff23f9 feat: auto-hide mobile buttons on scroll down
- Add useScrollDirection hook for scroll direction detection
- Hide bookmark and highlight buttons when scrolling down
- Show buttons again when scrolling up
- Smooth opacity transitions for better UX
- Only detect scroll when buttons are visible
- Improves mobile reading experience by maximizing content area
2025-10-11 01:39:24 +01:00
Gigi
72a264a01e feat: auto-close sidebar on mobile when navigating to content
- Add effect to close sidebar when route changes on mobile
- Handles clicking on blog posts in Explore view
- Complements existing sidebar auto-close for bookmarks and highlights
- Improves mobile UX by preventing sidebar from blocking content
2025-10-11 01:37:46 +01:00
Gigi
5a67be8096 docs: update CHANGELOG for v0.4.1 release 2025-10-10 21:46:30 +01:00
Gigi
9a929a6be4 chore: bump version to 0.4.1 2025-10-10 21:45:41 +01:00
Gigi
e0ca010026 feat: improve hero image rendering with zoom-to-fit on mobile 2025-10-10 21:44:51 +01:00
Gigi
8bd5d7aadf fix: move long article summaries below image on mobile to prevent overlay issues 2025-10-10 21:43:55 +01:00
Gigi
9115c38cde fix: improve article summary display on mobile devices 2025-10-10 21:40:15 +01:00
Gigi
0c7c1d54d9 feat: add nstart.me onboarding link for new users 2025-10-10 21:26:06 +01:00
Gigi
d529d83eb8 fix: add touch event support for highlight creation on mobile 2025-10-10 21:24:46 +01:00
Gigi
a3127c7836 docs: update CHANGELOG for v0.4.0 release 2025-10-10 18:07:57 +01:00
Gigi
4d5fe1f425 chore: bump version to 0.4.0 2025-10-10 18:07:06 +01:00
Gigi
c7a4de9786 Merge pull request #1 from dergigi/mobile
Add mobile responsive design
2025-10-10 18:04:54 +01:00
Gigi
d873718e88 fix: replace any type with proper bookmark interface for linter compliance 2025-10-10 18:03:48 +01:00
Gigi
706276839a fix: reduce mobile backdrop opacity and ensure sidepanes appear above it 2025-10-10 18:01:39 +01:00
Gigi
d281ca5f87 fix: force bookmarks pane expanded on mobile and ensure highlights pane sits above content on desktop 2025-10-10 17:54:32 +01:00
Gigi
6a9036bfef fix: add flex properties to mobile bookmark containers for proper filling 2025-10-10 17:25:40 +01:00
Gigi
1b242f75c6 fix: restore desktop grid layout for highlights panel 2025-10-10 17:24:26 +01:00
Gigi
7ffd37289d fix: improve empty state and loading visibility in mobile sidepanes 2025-10-10 17:23:12 +01:00
Gigi
cb859ae599 fix: restore flex layout to highlights pane for desktop view 2025-10-10 17:22:14 +01:00
Gigi
a17346c9c2 fix: ensure bookmarks container fills mobile sidepane properly 2025-10-10 17:21:06 +01:00
Gigi
c17a39588d refactor: DRY mobile sidepane styles - unified overlay behavior 2025-10-10 17:19:14 +01:00
Gigi
33cee9c0c2 feat: hide main content when sidepanes open on mobile for single-pane view 2025-10-10 17:11:26 +01:00
Gigi
e6d2920c27 feat: add mobile highlights panel as overlay with toggle button 2025-10-10 17:10:48 +01:00
Gigi
d8195dbe2a refactor: replace hamburger icon with bookmark icon on mobile 2025-10-10 17:08:36 +01:00
Gigi
4843f129c4 docs: update CHANGELOG with mobile implementation 2025-10-10 17:03:07 +01:00
Gigi
fcd1218dc4 docs: add comprehensive mobile implementation documentation 2025-10-10 17:02:46 +01:00
Gigi
eef0f971d7 fix: resolve TypeScript errors for mobile implementation 2025-10-10 17:01:57 +01:00
Gigi
ff09a8aba0 feat: add mobile auto-collapse setting 2025-10-10 17:00:52 +01:00
Gigi
0c4b523d05 feat: implement mobile overlay sidebar with focus trap and ESC handling 2025-10-10 17:00:03 +01:00
Gigi
de7a435a01 feat: add mobile-responsive CSS with breakpoints and safe areas 2025-10-10 16:57:56 +01:00
Gigi
124d399d1f feat: add mobile sidebar state management to useBookmarksUI 2025-10-10 16:56:19 +01:00
Gigi
e22cf71b15 feat: add media query hooks for responsive design 2025-10-10 16:55:53 +01:00
Gigi
670997ed36 feat: update viewport meta for mobile support 2025-10-10 16:55:39 +01:00
Gigi
1ccb6388e3 docs: update CHANGELOG for v0.3.8 2025-10-10 16:30:57 +01:00
Gigi
7d5be8d6aa chore: bump version to 0.3.8 2025-10-10 16:30:21 +01:00
Gigi
133e4756b2 fix: add vercel.json to handle SPA routing on Vercel
Without this configuration, page refreshes result in 404 errors because
Vercel tries to serve non-existent files instead of routing through
index.html for client-side routing.
2025-10-10 16:22:33 +01:00
Gigi
39ada734d5 docs: update CHANGELOG for v0.3.7 2025-10-10 13:25:18 +01:00
Gigi
19d88c5fba chore: bump version to 0.3.7 2025-10-10 13:24:31 +01:00
Gigi
461b0936e2 fix: use clearActive() method for logout instead of setActive(null)
Changed logout to use the proper clearActive() method from AccountManager instead of setActive(null), which was causing TypeScript type errors. This is the correct way to clear the active account according to the applesauce-accounts API.
2025-10-10 13:22:50 +01:00
Gigi
e9ee5e87be chore: add applesauce reference directory to gitignore
Added the applesauce directory to .gitignore to exclude the local reference copy of the applesauce monorepo from being committed to the project repository.
2025-10-10 13:21:25 +01:00
Gigi
5e66c5ef76 fix: correct logout functionality by using null instead of undefined
The logout button wasn't working because setActive was being called with 'undefined as never', which is an incorrect type hack. Changed to use null instead, which properly clears the active account. Also removed redundant localStorage.removeItem('active') call since the active$ subscription already handles localStorage cleanup.
2025-10-10 13:19:34 +01:00
Gigi
307dc3d726 docs: update CHANGELOG for v0.3.6 2025-10-10 13:16:05 +01:00
Gigi
e514a5f063 chore: bump version to 0.3.6 2025-10-10 13:14:41 +01:00
Gigi
880b7974f4 style: make connecting notification more subtle with muted blue background 2025-10-10 13:12:03 +01:00
Gigi
47048f435f Revert "fix(ui): prevent highlight panel UI breaks with long content or formatting"
This reverts commit a31f05d498.
2025-10-10 06:04:57 +01:00
Gigi
53ad492729 fix(ui): remove incorrect padding-right from highlights container 2025-10-09 21:31:17 +01:00
Gigi
eb4da419ae chore: update Boris pubkey for zap splits to npub19802see0gnk3vjlus0dnmfdagusqrtmsxpl5yfmkwn9uvnfnqylqduhr0x 2025-10-09 21:30:43 +01:00
Gigi
c66dfc9e2e feat(ui): use compact date format for highlights (now, 5m, 3h, 2d, 1mo, 1y) 2025-10-09 21:28:01 +01:00
Gigi
a31f05d498 fix(ui): prevent highlight panel UI breaks with long content or formatting 2025-10-09 21:27:08 +01:00
Gigi
6548e89c54 fix(ui): reduce font size of highlight metadata for cleaner look 2025-10-09 21:25:54 +01:00
Gigi
8a21b46ebd fix(ui): position highlight FAB button relative to article pane, not viewport 2025-10-09 21:23:21 +01:00
Gigi
bc5fe1ae30 fix(ui): adjust relay indicator position for better visual alignment 2025-10-09 21:22:02 +01:00
Gigi
b57ea3f640 fix(ui): ensure highlight metadata elements align on single visual line with consistent line-height 2025-10-09 21:18:14 +01:00
Gigi
3b55d64468 feat(ui): ultra-compact date format for bookmarks sidebar (now, 5m, 3h, 2d, 1mo, 1y) 2025-10-09 21:17:14 +01:00
Gigi
4caf1f0b22 fix(ui): prevent bookmark icons from being cut off in compact view 2025-10-09 21:16:20 +01:00
Gigi
1eb9911645 feat(highlights): encode event links as nevent/naddr per NIP-19 2025-10-09 21:15:03 +01:00
Gigi
38268c453c fix(ui): clean up nested borders in bookmark items for cleaner look 2025-10-09 21:13:47 +01:00
Gigi
9686b80b09 fix(ui): clean up nested borders in bookmarks sidebar view mode controls 2025-10-09 21:12:50 +01:00
Gigi
f32dec16fb fix(ui): align highlight metadata elements on single line in sidebar 2025-10-09 21:12:06 +01:00
Gigi
cb444b532f fix(explore): change header icon from compass to newspaper 2025-10-09 21:11:17 +01:00
Gigi
962062130a feat(routing): render /explore via Bookmarks to keep side panels 2025-10-09 21:10:51 +01:00
Gigi
e429931139 feat(layout): render Explore within ThreePaneLayout so side panels remain 2025-10-09 21:10:33 +01:00
Gigi
e56d28f82a chore: update highlight alt tag domain to read.withboris.com 2025-10-09 21:08:45 +01:00
Gigi
13a30d35c4 docs: update README app link to https://read.withboris.com/ 2025-10-09 21:08:31 +01:00
Gigi
e3174d8777 chore(seo): update robots.txt sitemap to https://read.withboris.com/ 2025-10-09 21:08:22 +01:00
Gigi
829a8d5dca chore(seo): update canonical and social URLs to https://read.withboris.com/ 2025-10-09 21:08:13 +01:00
Gigi
00978e2e64 chore: commit pending RelayStatusIndicator changes before URL update 2025-10-09 21:08:00 +01:00
Gigi
a5fcf36e83 docs: update CHANGELOG for v0.3.5 2025-10-09 20:28:50 +01:00
Gigi
a92a9ee3a3 chore: bump version to 0.3.5 2025-10-09 20:27:59 +01:00
Gigi
f39e34c699 fix: ensure connecting state shows for minimum 15s to prevent premature offline display 2025-10-09 20:27:20 +01:00
Gigi
b58f34d587 fix: add Cloudflare Pages routing config for SPA paths
Add _routes.json configuration to properly handle direct /r/ and /a/ paths
on Cloudflare Pages deployments. This ensures that client-side routes are
served correctly instead of returning 404 errors.
2025-10-09 20:22:08 +01:00
Gigi
76d1d4544e feat: extend connecting state to 8 seconds and remove subtitle text
- Increase 'Connecting' timeout from 4 to 8 seconds
- Remove explanatory subtitle 'Establishing connections...'
- Cleaner, simpler connecting state display
2025-10-09 20:17:29 +01:00
Gigi
5e56176e2d docs: update CHANGELOG for v0.3.3 and v0.3.4 2025-10-09 18:39:30 +01:00
Gigi
a2a4e7e454 chore: bump version to 0.3.4 2025-10-09 18:38:32 +01:00
Gigi
b266288b0f fix: add p tag (author tag) to highlights of nostr-native content
- Highlights now include p tag referencing original article author
- Allows authors to discover highlights of their work
- Follows NIP-84 best practices for highlight attribution
2025-10-09 18:36:20 +01:00
Gigi
1619e328da chore: bump version to 0.3.3 2025-10-09 18:34:12 +01:00
Gigi
b852dad243 fix: resolve linter errors for unused parameters
- Add eslint-disable comments for intentionally unused _settings parameters
- Parameters kept for API compatibility with existing code
- All linter and type checks now pass
2025-10-09 18:31:08 +01:00
Gigi
1552a5f106 feat: reorganize bookmarks UI - add explore button and move refresh
- Move refresh button from top bar to end of bookmarks list
- Show relative time of last fetch next to refresh button
- Add 'Explore' button (fa-newspaper icon) to top bar that links to /explore
- Track lastFetchTime in useBookmarksData hook
- Better UX with explore more prominent and refresh less intrusive
2025-10-09 18:29:41 +01:00
Gigi
0feaffb21b feat: make explore page article cards proper links
- Replace div+onClick with Link components
- Enable CMD+click to open articles in new tabs
- Preserve SPA navigation for normal clicks
- Better UX with standard browser link behavior
2025-10-09 18:27:18 +01:00
Gigi
9b3a4e20de feat: show 'Connecting' instead of 'Offline' on page load
- Display 'Connecting' with spinner for first 4 seconds after page load
- Give relays time to establish connections before showing 'Offline'
- Immediately switch to normal state once any relay connects
- Better UX - most refreshes aren't actually offline, just connecting
2025-10-09 18:26:01 +01:00
Gigi
c83b972a68 fix: correct TypeScript types for cache stats state 2025-10-09 18:24:49 +01:00
Gigi
2e96f93d81 refactor: simplify image caching to use Service Worker only
- Remove complex Cache API management with blob URLs and metadata
- useImageCache now simply returns the URL (Service Worker handles caching)
- imageCacheService reduced to just stats and clear functions
- Service Worker automatically caches all images on fetch
- Much simpler, DRY code that 'just works' for offline mode
- Stats now read directly from Cache API instead of localStorage metadata
2025-10-09 18:24:22 +01:00
Gigi
1e8182d984 feat: add Service Worker for robust offline image caching
- Implement Service Worker to intercept and cache image requests
- Service Worker persists across hard reloads unlike Cache API alone
- Simplify useImageCache hook to work with Service Worker
- Images now work offline even after hard reload
- Service Worker handles transparent cache-first serving for images
2025-10-09 18:17:27 +01:00
Gigi
b20a67d4d0 fix: improve image cache resilience for offline viewing
- Clean up stale metadata when Cache API doesn't have cached data
- Handle online/offline state properly in image loading
- Show original URL when online, blob URL from cache when offline
- Prevent cache misses when browser clears Cache API on hard reload
2025-10-09 18:15:30 +01:00
Gigi
60975b449d fix: import useEventModel from applesauce-react/hooks for proper type safety 2025-10-09 18:10:00 +01:00
Gigi
704fce4d80 fix: import Models from applesauce-core instead of applesauce-react 2025-10-09 18:07:45 +01:00
Gigi
4d1eb0f9fd fix: use correct useEventModel hook for profile loading in BlogPostCard 2025-10-09 18:03:32 +01:00
Gigi
ceafe277d3 feat: add /explore route to discover blog posts from friends
- Create exploreService to fetch kind:30023 events from followed users
- Add BlogPostCard component for displaying blog post previews
- Add Explore page component with grid layout
- Add /explore route to App.tsx (not linked in navigation yet)
- Add responsive CSS styles for explore page and blog post cards
- Clicking blog post cards navigates to article view
2025-10-09 18:02:07 +01:00
Gigi
8f2ecd5fe1 chore: bump version to 0.3.2 2025-10-09 17:49:08 +01:00
Gigi
d6be6f364b refactor: migrate image cache from localStorage to Cache API
BREAKING CHANGE: Image cache now uses Cache API instead of localStorage

Benefits:
- Support for actual 210MB cache size (localStorage limited to 5-10MB)
- Store native Response objects (no base64 overhead)
- Asynchronous, non-blocking operations
- Better suited for large binary blobs like images
- Can handle hundreds of MB to several GB

Changes:
- Rewrite imageCacheService to use Cache API for image storage
- Keep metadata in localStorage for LRU tracking (small footprint)
- Update useImageCache hook to handle async Cache API
- Add blob URL cleanup to prevent memory leaks
- Update clearImageCache to async function

The cache now works as advertised and won't hit quota limits.
2025-10-09 17:48:59 +01:00
Gigi
035d4d3bd0 chore: bump version to 0.3.1 2025-10-09 17:36:37 +01:00
Gigi
43d5554c0c feat: change default image cache size to 210MB
Increase default cache size from 50MB to 210MB for better offline experience
2025-10-09 17:31:53 +01:00
Gigi
724a3e5cfa refactor: move 'Rebroadcast events' setting to Startup & Behavior section
Move the rebroadcast setting from Flight Mode to Startup & Behavior as it's more about behavior than offline mode
2025-10-09 17:31:07 +01:00
Gigi
0c49988d36 refactor: rename 'Startup Preferences' to 'Startup & Behavior' 2025-10-09 17:30:40 +01:00
Gigi
70de68848b refactor: move image cache setting to top of Flight Mode section
Reorder settings so 'Use local image cache' appears before 'Use local relays as cache'
2025-10-09 17:29:37 +01:00
Gigi
8a12ae72cb fix: ensure cache size input uses same font and size as surrounding text
Use inherit for fontSize, fontFamily, and color to match parent styling
2025-10-09 17:29:03 +01:00
Gigi
f8d5d19a9f refactor: inline textbox in cache stats display
Move max cache size input inline with stats text: '( X MB / [input] MB used )'
2025-10-09 17:28:20 +01:00
Gigi
dbd20e676f refactor: use IconButton component for cache clear button
Replace inline styled button with existing IconButton component to keep code DRY
2025-10-09 17:27:53 +01:00
Gigi
bbdf47fb94 refactor: update cache stats display format
Change from '(X.X MB, N images)' to '( X.X MB / [ max ] MB used )'
2025-10-09 17:27:21 +01:00
Gigi
1b754e02dc refactor: update cache setting label to 'Use local image cache' 2025-10-09 17:26:52 +01:00
Gigi
a2e410252a refactor: condense cache settings to single line
- Combine all cache settings into one horizontal line
- Shorten 'Cache images for offline viewing' to 'Cache images'
- Shorten 'Max cache size (MB):' to 'Max (MB):'
- Simplify current stats display with parentheses
- Use flexbox with wrap for responsive layout
2025-10-09 17:26:13 +01:00
Gigi
c9a14d151d refactor: simplify image cache settings UI
- Remove 'Image Cache' heading
- Remove explanatory text about localStorage
- Replace slider with simple number input for cache size
- Replace 'Clear Cache' button text with trash icon
- Make cache stats display more compact
2025-10-09 17:25:18 +01:00
Gigi
b286562e86 fix: extend article hero image to pane edges
Remove padding/margins from article hero images so they extend all the way
to the top, left, and right edges of the article pane. Uses negative margins
to counteract the reader container's padding.
2025-10-09 17:24:24 +01:00
Gigi
507288f51c feat: add image caching for offline mode
- Add imageCacheService with localStorage-based image caching and LRU eviction
- Create useImageCache hook for React components to fetch and cache images
- Integrate image caching with article service to cache cover images on load
- Add image cache settings (enable/disable, size limit) to user settings
- Update ReaderHeader to use cached images for article covers
- Update BookmarkViews (CardView, LargeView) to use cached images
- Add image cache configuration UI in OfflineModeSettings with:
  - Toggle to enable/disable image caching
  - Slider to set cache size limit (10-200 MB)
  - Display current cache stats (size and image count)
  - Clear cache button

Images are cached in localStorage for offline viewing, with a configurable
size limit (default 50MB). LRU eviction ensures cache stays within limits.
2025-10-09 17:23:31 +01:00
Gigi
e08bc54f15 refactor(relay): adjust offline indicator polling to 5s 2025-10-09 17:01:20 +01:00
Gigi
4306069191 fix(relay): make offline indicator poll every 3s for better responsiveness 2025-10-09 17:00:53 +01:00
Gigi
56e56af8ec docs: update CHANGELOG for version 0.3.0 2025-10-09 16:59:05 +01:00
Gigi
4d65cd73a7 chore: bump version to 0.3.0 2025-10-09 16:56:56 +01:00
Gigi
d36d5b33b6 fix(lint): remove unused isLocalRelay import
Remove unused import to fix linter error
2025-10-09 16:56:36 +01:00
Gigi
4cd54834ce fix(highlights): update relay info after automatic sync completes
When offline sync completes successfully, update the highlight's publishedRelays to show all relays and change icon from plane to server. Previously only manual rebroadcast updated this info.
2025-10-09 16:51:10 +01:00
Gigi
1134a41192 fix(highlights): always show relay list in tooltip
Remove special case text - always show the actual list of relays. Use plane icon for local-only highlights but still show relay list in tooltip.
2025-10-09 16:47:25 +01:00
Gigi
aced38b147 fix(highlights): only show successfully reachable relays in flight mode
When creating highlights in flight mode (no remote connection), only show local relays in the relay indicator tooltip. Check connection status to determine which relays are actually reachable before setting publishedRelays field.
2025-10-09 16:41:45 +01:00
Gigi
82f52f73cc fix(highlights): always publish to all configured relays
Remove relay.connected filter when publishing/rebroadcasting. Now always attempts to publish to all configured relays and lets the relay pool handle connection state management. This ensures highlights are broadcast to all relays, not just those that report as connected at publish time.
2025-10-09 16:27:00 +01:00
Gigi
4239f50129 fix(highlights): include local relays in relay indicator tooltip
Remove filter that excluded local relays from the fallback tooltip - now shows all configured relays including localhost
2025-10-09 16:24:32 +01:00
Gigi
4e3bb36ea5 perf: reduce relay status polling interval to 20 seconds
Change relay status polling from 5s (default) and 2s (Settings/Indicator) to 20s across the board to reduce CPU usage and network requests
2025-10-09 16:23:59 +01:00
Gigi
0c58f4347b fix(highlights): show remote relay list for fetched highlights
Instead of 'no relay info', show the list of remote relays we're connected to as a fallback for highlights that don't have publishedRelays metadata (i.e., highlights fetched from other users)
2025-10-09 16:23:28 +01:00
Gigi
2dd0711a20 fix(types): add missing eventStore prop to ThreePaneLayoutProps
Add eventStore property to ThreePaneLayoutProps interface and import IEventStore to fix TypeScript errors
2025-10-09 16:22:06 +01:00
Gigi
53b3dd1c7f refactor(highlights): simplify relay indicator tooltip to show only relay list
Remove verbose text from tooltip - just show the relay URLs for debug purposes
2025-10-09 16:21:12 +01:00
Gigi
47e2204c3f fix(highlights): improve relay indicator tooltip accuracy
Update tooltip text to be more accurate about relay information:
- Show 'Published to X relays' for user-created highlights with publishedRelays
- Show 'Seen on X relays' for highlights with seenOnRelays tracking
- Show 'Fetched from network' for highlights without relay metadata
- Add seenOnRelays field to Highlight type for future relay tracking
2025-10-09 16:16:50 +01:00
Gigi
cc8b742731 fix(highlights): always show relay indicator icon
Previously the relay indicator was only shown for highlights with publishedRelays info (user-created highlights). Now it's always visible:
- Show server icon by default for all highlights
- Show plane icon for local-only/offline highlights
- Show spinner during rebroadcast/sync
- Always allow clicking to rebroadcast any highlight
2025-10-09 16:14:50 +01:00
Gigi
529fc6b630 fix(settings): make Relays heading same level as Flight Mode
Add section-title class to Relays heading to match Flight Mode formatting
2025-10-09 16:12:18 +01:00
Gigi
0c5c4b6c23 refactor(highlights): consolidate sync state into relay indicator
Show automatic rebroadcast/sync state in the relay indicator instead of separate meta spinner:
- Relay indicator shows spinner during both manual rebroadcast and auto-sync
- Update tooltip to distinguish between manual rebroadcast and auto-sync
- Remove redundant syncing indicator from meta area
- Clean up unused CSS for syncing indicator

This provides a single, consistent visual indicator for all relay broadcast states.
2025-10-09 16:11:33 +01:00
Gigi
d7320c4bc8 feat(highlights): add click-to-rebroadcast functionality to relay indicator
Make relay indicator icons clickable to trigger manual rebroadcast to all connected relays:
- Click plane icon (local/offline) to rebroadcast to remote relays
- Click server icon to rebroadcast to all relays
- Show spinner while rebroadcasting
- Update icon from plane to server on successful rebroadcast
- Keep plane icon on failure
- Pass relayPool and eventStore through component chain
- Add local state management for highlight updates in HighlightsPanel
- Enhance CSS with scale animation on hover/active
2025-10-09 16:10:43 +01:00
Gigi
98c107d387 refactor(highlights): consolidate relay/status indicators into single icon
Replace multiple redundant indicators (flight mode, local only, relay info) with a single relay indicator icon in the bottom-left of each highlight:
- Show plane icon for local-only or offline-created highlights
- Show server icon for highlights published to remote relays
- Keep spinner in meta area for actively syncing highlights
- Remove duplicate indicators from meta area
- Clean up unused CSS and imports
2025-10-09 16:07:50 +01:00
Gigi
ebe801ae92 fix(relays): keep all relay connections alive, not just local ones
Previously only local relays had keep-alive subscriptions, causing remote relays to disconnect when no active subscriptions were running. This made the app appear to be in flight mode even when online.

Now create a persistent subscription for all relays to maintain connections.
2025-10-09 16:05:26 +01:00
Gigi
d9730bb5f8 feat(highlights): add relay indicator icon to highlight items
Add a small server icon at the bottom-left of each highlight that shows which relays the highlight was published to. The icon appears when publishedRelays information is available (for user-created highlights) and displays a tooltip with the list of relay URLs on hover.

- Import faServer icon from FontAwesome
- Add relay indicator to HighlightItem component
- Display formatted relay list in tooltip
- Add CSS styling for the indicator with hover effects
- Support both dark and light modes
2025-10-09 16:04:24 +01:00
Gigi
6a142f5163 fix(highlights): publish to all connected relays instead of just one
The highlight creation service was getting all relays from the pool without checking if they were actually connected. This caused highlights to only be published to a subset of relays (sometimes just one).

Now properly filters relays using relay.connected to ensure highlights are published to all actually connected relays when online.
2025-10-09 16:01:16 +01:00
Gigi
2105dfe3f6 refactor: remove calendar icon from publication date
- Remove calendar icon (faCalendar) from publication date display
- Display only the formatted date text
- Remove icon-specific CSS styling (gap, svg styles)
- Cleaner, more minimal date display in top-right corner
2025-10-09 15:57:38 +01:00
Gigi
24c0889e9f refactor: add subtle border to publication date
- Add subtle white border with 20% opacity
- Rounded corners (6px border-radius)
- Helps define the date element without being too prominent
2025-10-09 15:56:47 +01:00
Gigi
db30c05aa0 refactor: simplify publication date styling
- Remove background and border from publication date
- Use white text with subtle drop shadow for all layouts
- Icon now uses drop-shadow filter for better visibility
- Cleaner, more minimal appearance that works well on any background
2025-10-09 15:56:26 +01:00
Gigi
4504377c36 feat: move publication date to top-right corner
- Position publication date in top-right corner of article header
- Works for both hero image and non-image layouts
- Add subtle background and border for better visibility
- On hero images: dark semi-transparent background with backdrop blur
- On regular headers: uses surface-secondary background
- Remove date from inline metadata (reading time and highlights remain)
2025-10-09 15:53:46 +01:00
Gigi
3c1114ad21 fix: resolve TypeScript type errors in offline sync
- Import IAccount from applesauce-accounts (not applesauce-core)
- Remove unused account parameter from syncLocalEventsToRemote
- Fix undefined vs null type mismatch in Bookmarks component
- All linter and type checks now pass cleanly
2025-10-09 15:52:34 +01:00
Gigi
e7c05b2c52 feat: keep local relay connections alive in flight mode
- Add persistent keep-alive subscription for local relays
- Prevents disconnection when no other subscriptions are active
- Uses minimal subscription (kinds: [0], limit: 0) to keep connection open
- Properly cleans up subscription on app unmount
- Resolves issue where local relays disconnect after idle period in flight mode
2025-10-09 14:08:37 +01:00
Gigi
ca35e4e7cc fix: plane icon now shows for offline-created highlights
- Add useEffect to watch highlight.isOfflineCreated prop changes
- State now updates when prop changes (not just on initial mount)
- Add isOfflineCreated to console log for easier debugging
- Fixes issue where plane icon wouldn't appear for new offline highlights

The bug was that showOfflineIndicator state was only set once during
component initialization. If the highlight prop didn't have isOfflineCreated
set at that moment, the icon would never appear even if the prop changed later.
2025-10-09 14:05:58 +01:00
Gigi
2d5e48a64e perf: make highlight creation instant by non-blocking relay publish
Major UX improvement:
- Store event in EventStore FIRST (before publishing)
- Return highlight immediately (no await on relay publish)
- Publish to relays in background asynchronously
- UI now updates instantly (<50ms) instead of waiting seconds

Before:
1. Create event
2. Wait for relay publish (1-5 seconds)
3. Store in EventStore
4. Return to UI

After:
1. Create event
2. Store in EventStore
3. Return to UI immediately 
4. Publish to relays in background

Benefits:
- Instant highlight appearance in UI
- No blocking on network operations
- Better perceived performance
- Especially noticeable in flight mode with slow local relays
- Event still saved even if publishing fails
2025-10-09 14:01:55 +01:00
Gigi
be86634a65 fix: skip rebroadcasting when in flight mode
- Check actual relay connectivity before rebroadcasting
- Skip rebroadcast to all relays if no remote relays connected
- Still allows rebroadcast to local relays in flight mode
- Prevents unnecessary publish attempts to unreachable relays
- Logs: '✈️ Flight mode: skipping rebroadcast to remote relays'

This prevents the app from trying to rebroadcast fetched events to
remote relays when only local relays are connected (flight mode).
2025-10-09 14:00:57 +01:00
Gigi
a2041bd14d feat: show sync progress and hide indicator after successful sync
- Show spinning blue icon while event is syncing to remote relays
- Hide offline indicator completely after successful sync
- Add sync state tracking with listeners for real-time updates
- Track successful vs failed syncs separately
- Only clear offline flag for successfully synced events
- Blue spinner (#3b82f6) indicates active sync
- Clean UI: no indicator after sync completes

Behavior:
1. Create highlight offline → plane icon
2. Come back online → spinner replaces plane
3. Sync completes → no indicator (clean)
4. Sync fails → plane icon returns
2025-10-09 13:56:12 +01:00
Gigi
d294287c64 refactor: use applesauce EventStore for offline event management
Major improvements:
- Store highlights in EventStore immediately when created
- Query EventStore instead of local relays for offline sync
- Pass eventStore to highlight creation service and hooks
- Simplified offline sync: no more relay queries, just EventStore lookups
- More efficient and reliable offline event tracking
- Better integration with applesauce architecture

Benefits:
- Faster sync (no relay queries needed)
- More reliable (events always in EventStore)
- Cleaner code (leveraging applesauce patterns)
- Better separation of concerns
2025-10-09 13:54:47 +01:00
Gigi
95162d4423 feat: add flight mode indicator to offline-created highlights
- Add isOfflineCreated property to Highlight type
- Set flag when highlight is created in local-only mode
- Display small plane icon in highlight sidebar for offline-created highlights
- Lighter amber color (#fbbf24) to distinguish from Local badge
- Tooltip: 'Created while in flight mode'
- Visual indicator helps users track which highlights need syncing
2025-10-09 13:51:04 +01:00
Gigi
4224c989c6 fix: improve offline sync with better tracking and logging
- Track events explicitly when created in offline mode
- Mark highlights as offline-created when isLocalOnly is true
- Add extensive debug logging throughout sync process
- Increase query timeout from 5s to 10s for better reliability
- Add 2-second delay before syncing to allow relays to connect
- Log relay state transitions and event counts
- Log each event received during sync query
- Should help diagnose and fix offline sync issues
2025-10-09 13:49:35 +01:00
Gigi
3330f22f82 fix: clean up sync state tracking in offline sync service
- Remove duplicate state tracking from service
- State transition detection now fully handled by hook
- Fix remaining syncState reference bug
- Simplify sync lock mechanism
2025-10-09 13:38:23 +01:00
Gigi
450776f9d0 feat: automatic offline sync - rebroadcast local events when back online
- Create offlineSyncService to sync local-only events to remote relays
- Create useOfflineSync hook to detect online/offline transitions
- When user comes back online (remote relays connect), automatically:
  - Query local relays for user's events from last 24 hours
  - Rebroadcast highlights and bookmarks to remote relays
- Integrate sync into Bookmarks component
- Enables seamless offline workflow:
  - User can work offline with local relays
  - Events are automatically synced when connection restored
  - No manual intervention required
2025-10-09 13:37:33 +01:00
Gigi
0478713fd5 refactor: rename 'Offline Mode' to 'Flight Mode'
- Change 'Local Only' to 'Flight Mode' in relay status indicator
- Rename settings section from 'Offline Mode' to 'Flight Mode'
- Better reflects the airplane icon metaphor
- More intuitive terminology for local-only relay mode
2025-10-09 13:34:39 +01:00
Gigi
0f2b94cc61 style: use wifi icon for disconnected remote relays
- Replace red dot (faCircle) with red wifi icon (faWifi)
- Better visual representation of network disconnection
- Icon size now consistent at 1rem across all states
2025-10-09 13:32:22 +01:00
Gigi
b511d40375 refactor: improve relay list display with airplane icons
- Remove separate 'Offline' section, show all relays in one list
- Local relays always shown first (sorted to top)
- Local relays use airplane icon instead of checkmark
- Airplane icon is green when connected, red when offline
- Remote relays use green checkmark (connected) or red dot (offline)
- Last seen timestamp shown only for disconnected relays
- Simplified layout for better readability
2025-10-09 13:31:09 +01:00
Gigi
d090b953bf fix: check actual relay connection status instead of pool membership
- Check relay.connected property to determine if relay is actually connected
- Previously only checked if relay was in pool, not if connection was active
- Add debug logging to help diagnose connection status issues
- This should fix the airplane indicator not showing when offline
- Relays should now correctly show as disconnected after being offline
2025-10-09 13:29:43 +01:00
Gigi
19595d19ca feat: improve font size scale and default
- Change font sizes from [14,16,18,20,22,24] to [16,18,21,24,28,32]
- Larger sizes now more spread out (28px and 32px)
- Set default font size to 21px instead of 18px
- Better progression for reading comfort
2025-10-09 13:19:07 +01:00
Gigi
239ebba439 style: improve publishing date display
- Use shorter date format (MMM d, yyyy instead of MMMM d, yyyy)
- Add subtle styling with reduced opacity and smaller font
- Make calendar icon smaller and more muted
- Style overlay version for hero images with subtle white text
2025-10-09 13:16:45 +01:00
Gigi
67c6b75cb7 feat: add 6th font size option for balanced UI
- Add 24px font size option
- Now has 6 font sizes to match 6 color options
- Better visual balance in settings UI
2025-10-09 13:13:40 +01:00
Gigi
502dbd801a feat: place Reading Font and Font Size settings side-by-side
- Wrap both settings in a flex container
- Reading Font takes flexible space with min-width
- Font Size takes only necessary space
- Responsive with flex-wrap for smaller screens
- Better use of horizontal space in settings UI
2025-10-09 13:12:57 +01:00
Gigi
e114223e46 refactor: simplify rebroadcast setting text
- Change 'Rebroadcast events to all relays' to 'Rebroadcast events while browsing'
- More concise and user-friendly wording
2025-10-09 13:11:25 +01:00
Gigi
a9c73d35ef feat: add localhost:4869 as second local relay
- Add ws://localhost:4869 to RELAYS configuration
- Update comment to reflect multiple local relays
- Support additional local relay option for users
2025-10-09 13:10:20 +01:00
Gigi
b8f20b73d1 refactor: simplify text 'local relay(s)' to 'local relays' 2025-10-09 13:08:52 +01:00
Gigi
dc8d687f0c refactor: move local relay info box to Offline Mode section
- Move recommendation text from Relays to Offline Mode section
- Info box about Citrine and nostr-relay-tray now appears at end of Offline Mode
- Remove unused handleLinkClick and useNavigate from RelaySettings
- Add handleLinkClick to OfflineModeSettings for clickable links
- Clean up unused onClose prop in RelaySettings
2025-10-09 13:08:24 +01:00
Gigi
3180fc7c73 refactor: move rebroadcast settings to new Offline Mode section
- Create new OfflineModeSettings component
- Move 'Use local relay(s) as cache' checkbox
- Move 'Rebroadcast events to all relays' checkbox
- Position Offline Mode section before Relays section
- Keep consistent checkbox styling
- Remove settings/onUpdate props from RelaySettings (no longer needed)
2025-10-09 13:07:09 +01:00
Gigi
a0cba9fb6f refactor: use consistent checkbox style for rebroadcast settings
- Match existing checkbox pattern from other settings
- Use setting-group, checkbox-label, and setting-checkbox classes
- Add proper id and htmlFor attributes for accessibility
- Consistent with LayoutNavigationSettings and other checkbox settings
- Keep code DRY with unified styling approach
2025-10-09 13:05:33 +01:00
Gigi
3483532944 refactor: simplify rebroadcast settings UI
- Remove icons from checkbox labels
- Shorten text to simple checkbox labels
- Cleaner, more minimal design
- Settings: 'Use local relay(s) as cache' and 'Rebroadcast events to all relays'
2025-10-09 13:04:50 +01:00
Gigi
db20e73ea3 refactor: integrate rebroadcast settings into Relays section
- Move rebroadcast checkboxes from separate section into Relays section
- Add plane and globe icons to rebroadcast settings
- Remove separate RelayRebroadcastSettings component
- Settings now flow better with rebroadcast options at top, relay list below
- Maintains all functionality while improving UI organization
2025-10-09 13:04:12 +01:00
Gigi
b055294afc feat: add relay rebroadcast settings for caching and propagation
- Add two new settings:
  - Use local relay(s) as cache (default: enabled)
  - Rebroadcast events to all relays (default: disabled)
- Create rebroadcastService to handle rebroadcasting events
- Hook into article, bookmark, and highlight fetching services
- Automatically rebroadcast fetched events based on settings:
  - Articles when opened
  - Bookmarks when fetched
  - Highlights when fetched
- Add RelayRebroadcastSettings component with plane/globe icons
- Benefits:
  - Local caching for offline access
  - Content propagation across nostr network
  - User control over bandwidth usage
2025-10-09 13:01:38 +01:00
Gigi
831cb18b66 feat: improve relay status responsiveness for flight mode
- Reduce relay connection window from 20 minutes to 10 seconds
- Change 'Recently Seen' section to 'Offline' with red styling
- Use red circle icon for offline relays instead of gray
- Poll relay status every 2 seconds in settings (faster feedback)
- Poll relay status every 2 seconds in status indicator
- Now when entering flight mode:
  - Local relay stays connected (green checkmark with plane icon)
  - All remote relays move to red 'Offline' section within 10 seconds
  - Status is highly responsive and clear
2025-10-09 12:49:37 +01:00
Gigi
bb51788a1d feat: add plane icon indicator for local relays in settings
- Show plane icon badge next to local relays in relay list
- Badge appears for both active and recently seen local relays
- Uses amber styling to match local-only mode theme
- Includes tooltip explaining the relay is local
- Makes it easy to identify local relays at a glance in settings
2025-10-09 12:48:18 +01:00
Gigi
4cf2ac9172 feat: add relay status indicator for local-only/offline mode
- Create RelayStatusIndicator component with plane icon for local-only mode
- Position indicator in bottom-left corner with amber styling
- Show when only local relays are connected or completely offline
- Hide indicator when remote relays are available (normal operation)
- Add pulsing globe icon animation to indicate checking for connection
- Include hover effects and smooth transitions
- Auto-adjust position when sidebar is collapsed
- Display relay count and clear status messages
2025-10-09 12:47:29 +01:00
Gigi
bdab9c06e4 fix: make highlight creation resilient to offline/flight mode
- Wrap relay publish in try-catch to handle failures gracefully
- Attempt to publish to local relay even when no relays are connected
- Always return highlight object even if publish fails completely
- Add detailed logging to track publish status and failures
- Mark highlights as local-only when publish fails or only local relays available
- Ensure UI always displays newly created highlights immediately
2025-10-09 12:45:51 +01:00
Gigi
6636d540aa feat: add offline highlight creation with local relay tracking
- Add relay tracking to Highlight type (publishedRelays, isLocalOnly fields)
- Create utility functions to identify local relays (localhost/127.0.0.1)
- Update highlight creation service to track which relays received the event
- Detect when highlights are only on local relays and mark accordingly
- Add visual indicator in UI for local-only highlights with amber badge
- Enable immediate display of highlights created offline
- Ensure highlights work even when only local relay is available
2025-10-09 12:40:04 +01:00
Gigi
aa8332831f docs: update CHANGELOG for versions 0.2.7-0.2.10 2025-10-09 12:35:05 +01:00
Gigi
4ea03c9042 chore: bump version to 0.2.10 2025-10-09 12:33:38 +01:00
Gigi
4720416f2c fix(settings): remove trailing slash from relay URLs
- Update formatRelayUrl to strip trailing / from URLs
- Cleaner display of relay addresses
2025-10-09 12:31:30 +01:00
Gigi
8ad9e652fb feat(settings): highlight active zap split preset
- Add isPresetActive function to detect current preset
- Add 'active' class to preset button matching current weights
- Organize presets in a central object for easier maintenance
- Users can now see which preset is currently applied
2025-10-09 12:31:02 +01:00
Gigi
98c72389e2 refactor(settings): rename 'Default View Mode' to 'Default Bookmark View'
- More descriptive label clarifying this controls bookmark display
- Better indicates what view is being configured
2025-10-09 12:30:18 +01:00
Gigi
e032f432dd refactor(settings): move Show highlights checkbox after Default Highlight Visibility
- Reorder settings for better logical flow
- Show highlights toggle now appears after visibility controls
- Positioned right before the preview section
2025-10-09 12:29:49 +01:00
Gigi
852465bee7 fix(settings): constrain Reading Font dropdown width
- Wrap FontSelector in setting-control div
- Prevents dropdown from stretching across full page width
- Matches layout of other inline controls like color pickers
2025-10-09 12:29:13 +01:00
Gigi
39d0147cfa feat(routing): add /settings route and URL-based settings navigation
- Add /settings route to router
- Derive showSettings from location.pathname instead of state
- Update onOpenSettings to navigate to /settings
- Update onCloseSettings to navigate back to previous location
- Track previous location to restore context when closing settings
- Remove showSettings state from useBookmarksUI hook
2025-10-09 12:27:43 +01:00
Gigi
10cc7ce9b0 refactor(settings): move Default Highlight Visibility to Reading & Display
- Move setting from Startup Preferences to Reading & Display section
- Position above preview so changes are immediately visible
- Update preview to respect defaultHighlightVisibility settings
- Each highlight level (mine/friends/nostrverse) now toggles in preview
2025-10-09 12:24:50 +01:00
Gigi
6b8442ebdd refactor(settings): combine relay info into single paragraph
- Merge two separate paragraphs into one continuous text
- Remove line break between relay recommendations and educational links
2025-10-09 12:23:40 +01:00
Gigi
5aba283e92 refactor(settings): use sidebar-style colored buttons for highlight visibility
- Replace generic IconButton with colored level-toggle-btn elements
- Match the UI style from HighlightsPanelHeader in sidebar
- Show highlight colors (purple, orange, yellow) when active
- Use same CSS classes and structure for visual consistency
2025-10-09 12:23:05 +01:00
Gigi
59df232e2e refactor(settings): simplify Relays section by removing summary text
- Remove 'X active relays' summary count
- Remove 'Active' heading for cleaner UI
- Keep 'Recently Seen' heading for context since those are different
2025-10-09 12:21:36 +01:00
Gigi
702c001d46 feat(settings): add educational links about relays in reader view
- Add message with links to learn about relays (nostr.how and substack article)
- Links open in Boris's reader view via /r/ route instead of external tabs
- Close settings panel when links are clicked to show the content
- Use react-router navigation for seamless in-app experience
2025-10-09 12:21:09 +01:00
Gigi
48a9919db8 feat(reader): display article publication date
- Add published field to ReadableContent interface
- Pass published date from article loader through component chain
- Display formatted publication date in ReaderHeader with calendar icon
- Format date as 'MMMM d, yyyy' using date-fns
2025-10-09 12:15:28 +01:00
Gigi
d6d0755b89 feat(settings): add local relay recommendations to Relays section
- Add informational message recommending Citrine or nostr-relay-tray
- Include direct links to download pages for both local relay options
2025-10-09 12:13:41 +01:00
Gigi
facdd36145 feat(settings): add Relays section showing active and recently connected relays
- Add relayStatusService to track relay connections with 20-minute history
- Add useRelayStatus hook for polling relay status updates
- Create RelaySettings component to display active and recent relays
- Update Settings and ThreePaneLayout to integrate relay status display
- Shows relay connection status with visual indicators and timestamps
2025-10-09 12:09:53 +01:00
Gigi
5d379a280b chore: bump version to 0.2.9 2025-10-08 13:14:28 +01:00
Gigi
22a02d228d fix: deduplicate highlights in streaming callbacks
- Replace array accumulation with Map keyed by highlight ID
- Prevents duplicate highlights when same event arrives from multiple relays
- Fix applied in useArticleLoader and useBookmarksData hooks
- Only updates UI when new unique highlight arrives
- Resolves issue where highlights appeared twice in sidebar
2025-10-08 12:55:27 +01:00
Gigi
61fd5bbadc chore: bump version to 0.2.8 2025-10-08 12:42:24 +01:00
Gigi
d642c87527 fix: pass article summary through to ReadableContent
- Add summary field when converting ArticleContent to ReadableContent
- Fix contentLoader.ts to include article.summary
- Fix useArticleLoader.ts to include article.summary
- Article summaries now properly display in reader header
- Resolves missing summary display for kind:30023 articles
2025-10-08 12:41:03 +01:00
Gigi
fea425b5d0 feat: display article summary in header
- Add summary field to ReadableContent interface
- Pass summary through ContentPanel to ReaderHeader
- Display summary below title in both overlay and standard layouts
- Style summary with reading font for consistency
- Summary appears in white with shadow in image overlays
- Summary appears in gray (#aaa) in standard headers
- Enhances article preview and reading experience
2025-10-08 12:35:05 +01:00
Gigi
1609c6e580 feat: overlay title and metadata on hero images
- Position title and metadata absolutely over hero images
- Add gradient background for text readability (dark at bottom)
- Use backdrop-filter blur for metadata badges
- White text with shadow for better contrast
- Maintain original layout when no image present
- Creates more immersive reading experience
2025-10-08 12:30:00 +01:00
Gigi
270ea94c70 feat: apply reading font to article titles
- Add font-family: var(--reading-font) to .reader-title class
- Ensures consistent typography between titles and body text
- Titles now respect user's reading font preference from settings
2025-10-08 12:09:36 +01:00
Gigi
83e2f23357 chore: update homepage URL to read.withboris.com 2025-10-08 12:08:11 +01:00
Gigi
9df0261071 fix: correct Jina AI Reader proxy URL format
- Remove hardcoded http:// prefix in proxy URL
- Preserve original protocol (http/https) when constructing proxy URL
- Fix: https://r.jina.ai/https://example.com instead of /http://example.com
- Resolves metadata fetching issues for HTTPS URLs
2025-10-08 12:00:03 +01:00
Gigi
1dfe66651a refactor: reorder toolbar buttons
- New order: Profile, Home, Settings, Refresh, Plus, Logout
- Navigation first (Home, Settings)
- Actions in middle (Refresh, Plus)
- Logout at end
2025-10-08 11:58:28 +01:00
Gigi
dcb7933ede refactor: reorder toolbar buttons
- New order: Profile, Home, Refresh, Add, Settings, Logout
- Groups navigation (Profile, Home) at start
- Action buttons (Refresh, Add) in middle
- Settings and Logout at end
2025-10-08 11:48:19 +01:00
Gigi
aa72ac44c8 chore: bump version to 0.2.7 2025-10-08 11:45:52 +01:00
Gigi
44fb07033b refactor: reorder toolbar buttons for better UX
- New order: User, Home, Settings, Add, Refresh, Logout
- Profile/user first for identity prominence
- Core navigation (Home, Settings) before actions
- Actions (Add, Refresh) grouped together
- Logout at the end as final action
2025-10-08 11:45:07 +01:00
Gigi
7e2d412869 refactor: swap refresh and profile button positions in toolbar
- Move profile avatar before refresh button
- New order: Home, Add, Profile, Refresh, Settings, Logout/Login
- Improves toolbar organization and button flow
2025-10-08 11:39:50 +01:00
Gigi
19021af49a fix: revert to fetchReadableContent to avoid CORS issues
- url-metadata package doesn't work in browser due to CORS
- Restore use of fetchReadableContent with Jina AI proxy
- Extract helper functions for cleaner metadata parsing
- Maintain same functionality with proper browser compatibility
- Remove url-metadata dependency (25 packages)
2025-10-08 11:16:59 +01:00
Gigi
bdbb89c50e feat: only add boris tag when metadata is auto-extracted
- Start with empty tags field instead of pre-filling 'boris'
- Track if any metadata was successfully extracted (title, description, or tags)
- Only add 'boris' tag if we extracted something automatically
- Makes the boris tag more meaningful and intentional
- User can still manually add tags for URLs without metadata
2025-10-08 11:15:34 +01:00
Gigi
687f60db3f refactor: DRY tag extraction with normalizeTags helper
- Create normalizeTags helper to eliminate duplication
- Handle both string and array tag formats uniformly
- Reduce tag extraction code from 25 lines to 10 lines
- Cleaner, more maintainable code
2025-10-08 11:12:34 +01:00
Gigi
8ee7d347be refactor: use url-metadata package for robust metadata extraction
- Replace manual regex-based HTML parsing with url-metadata package
- Cleaner code with proper handling of OpenGraph, Twitter Cards, and standard meta tags
- Better handling of keywords (supports both string and array formats)
- More reliable extraction across different website structures
- Removes dependency on fetchReadableContent for metadata
- Significantly reduces code complexity (60+ lines to ~20 lines)
2025-10-08 11:10:10 +01:00
Gigi
8e9242e6f2 feat: auto-extract tags from metadata and add boris as default tag
- Set 'boris' as default tag in bookmark creation modal
- Extract tags from keywords meta tag (comma/semicolon separated)
- Extract tags from OpenGraph article:tag properties
- Deduplicate and limit to 5 extracted tags
- Prepend 'boris' to any extracted tags
- Only extract tags if user hasn't modified the tags field
- Use ref to prevent re-fetching same URL
- Improves bookmark organization and discoverability
2025-10-08 11:06:14 +01:00
Gigi
1df3962064 fix: improve modal spacing with proper box-sizing
- Add box-sizing: border-box to modal-content
- Add box-sizing: border-box to form inputs and textareas
- Ensures padding is included in width calculation
- Fixes right margin and prevents content from touching edges
2025-10-08 11:04:44 +01:00
Gigi
4edc22cec2 feat: prioritize OpenGraph tags for metadata extraction
- Extract title with priority: og:title > twitter:title > <title>
- Extract description with priority: og:description > twitter:description > meta description > first <p>
- OpenGraph tags provide better, curated metadata for sharing
- Twitter Card tags as fallback for social media compatibility
- Improved metadata quality for most modern websites
2025-10-08 11:01:51 +01:00
Gigi
82977fa5d4 feat: auto-fetch title and description when URL is pasted
- Automatically fetch page metadata using r.jina.ai proxy
- Debounced (800ms) to avoid API spam while typing
- Only auto-fills if fields are empty (won't overwrite user input)
- Extracts title from page
- Extracts description from meta tag or first paragraph
- Shows spinner indicator while fetching
- Gracefully handles fetch errors (just skips auto-fill)
- Uses existing fetchReadableContent service
2025-10-08 11:00:51 +01:00
Gigi
1a84817453 perf: publish bookmarks to relays in background
- Remove await on relayPool.publish() to not block UI
- Bookmark modal now closes immediately after signing
- Publishing happens asynchronously in the background
- Added error handling for failed relay publishes
- Fixes slow save issue caused by waiting for all 12 relays
2025-10-08 10:02:04 +01:00
Gigi
a0b98231b7 feat: add tags support to web bookmarks per NIP-B0
- Added tags input field to bookmark modal (comma-separated)
- Updated createWebBookmark to accept tags array
- Tags are added as 't' tags per NIP-B0 spec
- Added published_at tag with current timestamp
- Moved description to content field (per spec, not summary tag)
- d tag now uses URL without scheme (host + path + search + hash)
- Added helper text to explain tag formatting
- Styled form-helper-text for better UX
2025-10-08 09:51:33 +01:00
Gigi
d452f96f79 fix: pass relayPool as prop instead of using non-existent hook
- Fixed crash when opening bookmark bar
- Removed non-existent Hooks.useRelayPool() call
- Added relayPool prop to SidebarHeader, BookmarkList, and ThreePaneLayout
- Threaded relayPool through component hierarchy from Bookmarks down
- Web bookmark creation now works correctly
2025-10-08 09:49:02 +01:00
Gigi
dcf43cfce1 feat: add web bookmark creation (NIP-B0, kind:39701)
- Created webBookmarkService for creating web bookmarks
- Added AddBookmarkModal component with URL, title, and description fields
- Added plus button to sidebar header (visible when logged in)
- Modal validates URL format and publishes to relays
- Auto-refreshes bookmarks after creation
- Styled modal with dark theme matching app design
- Follows NIP-B0 spec: URL in 'd' tag, title and summary tags
2025-10-08 09:44:45 +01:00
Gigi
815b3cc57d fix: correct type signature for addZapTags function
- Changed event parameter type from NostrEvent to { tags: string[][] }
- Allows function to accept both EventTemplate and NostrEvent
- Fixes TypeScript error where EventTemplate was passed before signing
- No functional changes, just type safety improvement
2025-10-08 09:27:36 +01:00
Gigi
7e54a01237 feat: add zap split preset buttons
- Added 4 preset buttons: Default, Generous, Selfless, and Boris 🧡
- Default: 50/2.1/50 (You: 49%, Author: 49%, Boris: 2%)
- Generous: 5/10/75 (You: 6%, Author: 83%, Boris: 11%)
- Selfless: 1/19/80 (You: 1%, Author: 80%, Boris: 19%)
- Boris 🧡: 10/80/10 (You: 10%, Author: 10%, Boris: 80%)
- One-click setup for common zap split configurations
- Hover tooltips show actual percentage splits
2025-10-08 09:25:37 +01:00
Gigi
ec4692da15 fix: prevent sliders from jumping when resetting settings
- Added debouncing (300ms) to settings auto-save
- Added flag to prevent external settings updates during local editing
- External updates are blocked for 500ms after save completes
- Fixes issue where rapid save/subscription cycle caused sliders to jump
- Settings now update smoothly when resetting to defaults
2025-10-08 07:14:06 +01:00
Gigi
f6d2f98eae refactor: make zap split sliders independent using weights
- Changed from percentage-based to weight-based zap splits
- All three sliders (highlighter, author, Boris) are now independent
- Weights are normalized to calculate actual percentages
- UI shows both weight value and calculated percentage
- Added migration logic for users with old percentage-based settings
- Each slider can be adjusted without affecting the others
- Prevents interdependent slider behavior that was confusing

Breaking change: Settings now use zapSplitHighlighterWeight,
zapSplitAuthorWeight, and zapSplitBorisWeight instead of
zapSplitPercentage and borisSupportPercentage
2025-10-08 07:06:42 +01:00
Gigi
9b97715274 feat: add Boris support percentage to zap splits
- Added borisSupportPercentage setting (default 2.1%)
- Added separate slider in ZapSettings for Boris support
- Updated zap split calculation to include three-way split:
  - Highlighter gets their configured percentage
  - Boris gets their support percentage (0-10%)
  - Author(s) get remaining percentage, split proportionally
- Display all three percentages in the UI
- Updated addZapTags to include Boris as zap recipient
- Boris support is optional and adjustable (0-10% range)
2025-10-08 07:03:47 +01:00
Gigi
fa1e536a26 refactor: move zap splits to dedicated settings section
- Created new ZapSettings component as separate section
- Moved zap split slider from ReadingDisplaySettings
- Placed at end of settings page as requested
- Updated description to mention multiple authors support
2025-10-08 07:02:02 +01:00
Gigi
238aac1921 docs: add zap splits feature to README 2025-10-08 06:55:32 +01:00
Gigi
29edd159e7 feat: respect existing zap tags in source content when creating highlights
- Updated addZapTags function to check for existing zap tags in source event
- When source has zap tags (author group), split proportionally:
  - Highlighter gets their configured percentage
  - Remaining percentage distributed among existing authors proportionally
- Example: 50/50 split with 2 source authors = 50% highlighter, 25% each author
- Falls back to simple two-way split if no existing zap tags
- Prevents duplicate entries if highlighter is already in author group
2025-10-08 06:53:53 +01:00
Gigi
a3edb64e4c docs: add CHANGELOG.md based on git history 2025-10-08 06:43:08 +01:00
Gigi
1609416e1e chore: bump version to 0.2.6 2025-10-08 06:40:39 +01:00
Gigi
00d9fbdbab feat: add home button to bookmark bar
- Add home icon button to sidebar header
- Clicking home button navigates to root path '/'
- Position home button before refresh and settings buttons
2025-10-08 06:34:22 +01:00
Gigi
239ab5763d feat: add configurable zap split for highlights on nostr-native content
- Add zapSplitPercentage setting (default 50%) to UserSettings
- Implement NIP-57 Appendix G zap tags for highlight events
- Add zap tags when creating highlights of nostr-native content
- Split zaps between highlighter and article author based on setting
- Add UI slider in settings to configure split percentage
- Include relay URL in zap tags for metadata lookup
- Only add author zap tag if different from highlighter
2025-10-08 06:32:02 +01:00
Gigi
f4f0de3508 chore(release): v0.2.5 2025-10-07 22:19:49 +01:00
Gigi
0c3e697df6 fix(reader): wire preview ref to markdown conversion hook
- Update useMarkdownToHTML to return {renderedHtml, previewRef}
- Use returned previewRef in ContentPanel hidden markdown preview
- Resolves article markdown not rendering instantly
2025-10-07 22:16:31 +01:00
Gigi
a9847a8848 fix: add missing useEffect dependencies for article loading
- Fix useBookmarksData hook to include all callback dependencies
- Fix useArticleLoader hook to include all setter function dependencies
- Fix useExternalUrlLoader hook to include all setter function dependencies
- Ensures articles load properly when navigating
2025-10-07 22:04:12 +01:00
Gigi
5ef7d2c41c refactor: DRY up highlight classification and URL normalization
- Extract classifyHighlight and classifyHighlights utilities
- Remove duplicate highlight classification logic from 3 locations
- Use existing normalizeUrl from urlHelpers instead of duplicating
- Reduce code duplication while maintaining functionality
2025-10-07 21:58:17 +01:00
Gigi
e1f704a690 fix: resolve linter errors
- Remove unused imports in ReadingDisplaySettings and useBookmarksData
- Add eslint-disable comments for unavoidable any types from applesauce library
- All lint and type-check validations now pass
2025-10-07 21:57:04 +01:00
Gigi
59ecc29b9c refactor(highlights): split highlighting utilities into modules
- Create textMatching module for text search utilities
- Create domUtils module for DOM manipulation helpers
- Create htmlMatching module for HTML highlight application
- Reduce highlightMatching.tsx from 217 lines to 59 lines
- All files now under 210 lines
2025-10-07 21:54:41 +01:00
Gigi
9ae918f744 refactor(highlights): extract highlights panel components
- Create useFilteredHighlights hook for highlight filtering
- Extract HighlightsPanelCollapsed component
- Extract HighlightsPanelHeader component
- Reduce HighlightsPanel.tsx from 232 lines to 118 lines
2025-10-07 21:53:24 +01:00
Gigi
ac71d0b5a4 refactor(content): extract content rendering hooks
- Create useMarkdownToHTML hook for markdown conversion
- Create useHighlightedContent hook for highlight processing
- Create useHighlightInteractions hook for highlight interactions
- Reduce ContentPanel.tsx from 294 lines to 159 lines
2025-10-07 21:52:05 +01:00
Gigi
7c0d3b909b refactor(settings): split Settings into section components
- Extract ReadingDisplaySettings component
- Extract LayoutNavigationSettings component
- Extract StartupPreferencesSettings component
- Reduce Settings.tsx from 295 lines to 104 lines
2025-10-07 21:50:35 +01:00
Gigi
7b390aae66 refactor(highlights): extract event processing utilities
- Create highlightEventProcessor module with eventToHighlight utility
- Extract dedupeHighlights and sortHighlights functions
- Reduce highlightService.ts from 346 lines to 186 lines
- Eliminate repetitive event-to-highlight conversion code
2025-10-07 21:49:01 +01:00
Gigi
70a830fb66 refactor(bookmarks): split Bookmarks.tsx into smaller hooks and components
- Extract useBookmarksData hook for data fetching
- Extract useContentSelection hook for content selection logic
- Extract useHighlightCreation hook for highlight creation
- Extract useBookmarksUI hook for UI state management
- Create ThreePaneLayout component to reduce JSX complexity
- Reduce Bookmarks.tsx from 392 lines to 209 lines
2025-10-07 21:47:59 +01:00
Gigi
eee7628096 fix: encode/decode URLs in /r/ route to preserve special characters
URLs with special characters like // in https:// were being collapsed by browsers when passed directly in the path. Now using encodeURIComponent when navigating and decodeURIComponent when extracting the URL from the path.
2025-10-07 21:40:43 +01:00
Gigi
298e80cd49 chore: add public assets and deployment configuration
- Add _headers for security headers and asset caching
- Add robots.txt for SEO with sitemap reference
- Add _redirects for SPA client-side routing support
2025-10-07 21:34:46 +01:00
Gigi
5b7ad1d697 chore: add domain configuration for https://xn--bris-v0b.com/
- Add homepage field to package.json
- Add meta tags for SEO and social sharing (Open Graph, Twitter Card)
- Add canonical URL
- Add description meta tag
2025-10-07 21:34:17 +01:00
Gigi
0b5bf59e98 feat: hide bookmarks without content or URL 2025-10-07 18:37:45 +01:00
Gigi
a72a3e3f77 docs: add live app domain to README 2025-10-07 07:19:52 +01:00
Gigi
4b67826b9b chore(release): 0.2.4 2025-10-07 06:57:49 +01:00
Gigi
4184561583 chore: cleanup after build fixes (remove shims, update locks) 2025-10-07 06:57:42 +01:00
Gigi
ac5b9d339e chore: stop tracking node_modules/dist; fix accidental applesauce-core mutation 2025-10-07 06:48:19 +01:00
Gigi
58596b2998 chore: add .gitignore for node_modules and dist 2025-10-07 06:48:09 +01:00
Gigi
06116c3665 chore: update deps (npm update) and dedupe; lint+types clean 2025-10-07 06:47:01 +01:00
Gigi
8285df487f revert: use documented applesauce imports; remove alias shim
- Import EventStore from 'applesauce-core'
- Remove Vite alias/shim and node:path reference
- Keep config minimal and standard-compliant
2025-10-07 06:24:33 +01:00
Gigi
d3fad33948 build(vite): alias async-event-store to shim to avoid Vercel resolution error
- Add alias for applesauce-core/dist/event-store/async-event-store.js
- Provide minimal shim module so bundler resolves cleanly
- Use node:path and import.meta.url to build absolute path
- Keep linter strict (added ts-expect-error for node path types)
2025-10-07 06:22:54 +01:00
Gigi
6bf31c124c build: patch applesauce-core dist to avoid async-event-store re-export during bundling
- Remove re-export of async-event-store.js in event-store/index.js
- This is applied via patch-package in postinstall later (to be added)
2025-10-07 06:21:21 +01:00
Gigi
f455f61795 build: deep import EventStore to bypass re-export resolution on Vercel
- Import EventStore from applesauce-core/dist/event-store/event-store.js
- Add TS module declaration shim for deep import typing
- No functional changes, fixes Vercel bundling for async-event-store.js
2025-10-07 06:15:48 +01:00
Gigi
1d4ad4e2fa revert: remove custom applesauce resolver plugin from Vite config
- Remove applesauceResolver workaround
- Keep Vite config clean and standards-compliant
- We'll address applesauce-core resolution via proper imports and package fixes
2025-10-07 06:07:52 +01:00
Gigi
c691e83cb6 config: add custom plugin to resolve applesauce-core internals
- Create applesauceResolver plugin to manually resolve relative imports
- Bypasses restrictive exports field in applesauce-core@4.0.0
- Fixes Rollup resolution error for ./async-event-store.js
- Workaround for npm package exports bug blocking internal imports
2025-10-07 06:05:52 +01:00
Gigi
b4ceaceedc refactor: use main package exports instead of subpath imports
- Import Helpers from 'applesauce-core' instead of 'applesauce-core/helpers'
- Import Blueprints from 'applesauce-factory' instead of 'applesauce-factory/blueprints'
- Use namespace exports as documented in applesauce tutorial
- Fixes Vercel build issue with internal module resolution
- Follows recommended import patterns from applesauce docs
2025-10-07 06:04:07 +01:00
Gigi
a5710b3611 config: force SSR noExternal for applesauce packages
- Add ssr.noExternal to force pre-bundling of applesauce packages
- Add mainFields to resolve module entry points
- Add rollupOptions to ensure ESM output format
- Workaround for restrictive exports map in applesauce-core@4.0.0
2025-10-07 05:58:52 +01:00
Gigi
2cea8fc2fa config: add module resolution options for Vercel build
- Add resolve conditions for better ESM handling
- Configure esbuildOptions with resolveExtensions
- Add commonjsOptions to handle mixed ESM/CJS modules
- Attempt to fix applesauce-core async-event-store resolution issue
2025-10-07 05:57:36 +01:00
Gigi
6958ee7d66 chore: remove applesauce reference folder from git tracking
- applesauce folder is in .gitignore and used only for local reference
- npm packages are used in production, not the local workspace
2025-10-07 05:54:15 +01:00
Gigi
7af0827019 config: add explicit module resolution for Vercel
- Add resolve.extensions to help with .js module resolution
- Include applesauce packages in optimizeDeps for better bundling
- Attempt to fix Vercel build issue with async-event-store.js
2025-10-07 05:52:16 +01:00
Gigi
559c288215 build: update dist with manual NIP-78 event creation 2025-10-07 05:50:26 +01:00
Gigi
85aa77dba8 revert: back to manual NIP-78 event creation
- AppDataBlueprint is not available in npm package (only in local workspace)
- Revert to manual event construction using factory.create()
- This approach works on Vercel's build environment
- Added comment explaining why we can't use the blueprint
2025-10-07 05:48:50 +01:00
Gigi
803df2d22b refactor: use AppDataBlueprint via namespace import
- Import blueprints as namespace: 'import * as Blueprints from applesauce-factory/blueprints'
- Use Blueprints.AppDataBlueprint with proper factory API
- Cleaner than manual event construction
- Should work with Vercel as namespace imports don't require direct named exports
2025-10-07 05:46:20 +01:00
Gigi
a84fbe97b0 fix: create NIP-78 events manually without blueprint import
- Remove AppDataBlueprint import that's not available in npm package
- Create application data events directly using factory.create()
- Manually construct event with kind 30078, d-tag identifier, and JSON content
- Fixes Vercel build by avoiding unavailable blueprint exports
2025-10-07 05:44:59 +01:00
Gigi
21545fc76a fix: avoid deep imports for Vercel compatibility
- Define APP_DATA_KIND constant inline (30078 for NIP-78)
- Implement getAppDataContent helper inline to parse JSON
- Import AppDataBlueprint from 'applesauce-factory/blueprints' main export
- Fixes Vercel build errors with deep package imports
2025-10-07 05:39:41 +01:00
Gigi
303d5a0ac7 fix: correct AppDataBlueprint import path for Vercel build
- Change import from 'applesauce-factory/blueprints' to 'applesauce-factory/blueprints/app-data'
- Fixes TypeScript error TS2305 in production build
2025-10-07 05:36:45 +01:00
Gigi
59b7816312 chore: bump version to 0.2.3 2025-10-07 05:35:32 +01:00
Gigi
3f1066ca71 build: update production build artifacts
- Update dist/index.html with latest changes
- Includes all features from recent commits
2025-10-07 05:34:20 +01:00
Gigi
744642e2b7 fix: ensure bookmarks are sorted newest first after merging lists
- Re-sort all individual bookmarks after flattening from multiple lists
- Sort by added_at first, then fall back to created_at
- Prevents interleaving of sorted lists from breaking overall order
- Ensures newest bookmarks always appear at the top
2025-10-07 05:25:50 +01:00
Gigi
fd28a6e171 feat: parse and display summary tag for nostr articles
- Extract 'summary' tag from kind:30023 article bookmarks
- Display summary in place of truncated content for articles
- Show summary in all view modes (compact, cards, large)
- Add article-summary CSS class for potential styling
- Follows NIP-23 long-form content specification
2025-10-07 05:12:11 +01:00
Gigi
0124de8318 feat: merge and flatten bookmarks from multiple lists
- Extract all individual bookmarks from all bookmark lists
- Display them in a single flat list (already sorted by date in service)
- Remove wrapper metadata like 'N bookmarks in this list'
- Show all bookmarks together, newest first
- Implements NIP-51 and NIP-B0 bookmark list merging
2025-10-07 05:06:00 +01:00
Gigi
b37aac0a33 fix: hide empty bookmarks without content
- Filter out individual bookmarks that have no content
- Keep articles (kind:30023) and web bookmarks (kind:39701) even if empty
- Prevents display of placeholder items showing only icons and timestamps
2025-10-07 05:02:36 +01:00
Gigi
81ef047a31 chore: remove created date from bookmark list display
- Remove bookmark-meta div showing creation timestamp
- Cleaner UI without redundant date information
2025-10-07 04:59:55 +01:00
Gigi
704033e6cb fix: remove encrypted cyphertext display from bookmark list
- Remove display of top-level bookmark.content and bookmark.parsedContent
- These fields contain encrypted data that shouldn't be shown to users
- Individual bookmarks are already displayed properly within each list
2025-10-07 04:56:42 +01:00
Gigi
d59d27419e feat: update URL path when opening bookmarks from sidebar
- Add URL navigation when selecting bookmarks
- Navigate to /a/:naddr for nostr articles (kind:30023)
- Navigate to /r/:url for external URLs
- Encode article bookmarks to naddr format on selection
2025-10-07 04:55:30 +01:00
Gigi
f8d3fac149 chore: bump version to 0.2.2 2025-10-06 20:57:42 +01:00
Gigi
61e948f6a4 refactor: use icon toggle buttons for highlight visibility settings
- Replace checkboxes with IconButton components matching existing UI pattern
- Use faNetworkWired, faUserGroup, and faUser icons
- Maintain consistent visual style with other settings toggles
2025-10-06 20:55:56 +01:00
Gigi
22323591c9 feat: add default highlight visibility settings
- Add defaultHighlightVisibilityNostrverse/Friends/Mine to UserSettings interface
- Add toggle controls in Settings page under Startup Preferences section
- Apply default visibility settings on app startup in Bookmarks component
- Users can now set which highlight levels (nostrverse/friends/mine) should be visible by default
2025-10-06 20:46:33 +01:00
Gigi
1b548cee3c feat: add proxy.nostr-relay.app relay to configuration 2025-10-06 20:43:12 +01:00
Gigi
fbb8fbdc20 fix: handle web bookmarks with URLs in d tag and prevent crash
- Extract URL from 'd' tag for kind:39701 web bookmarks
- Add protocol prefix (https://) if missing from web bookmark URLs
- Make classifyUrl handle undefined input gracefully
- Prevent crash when web bookmarks have no content
2025-10-06 20:34:37 +01:00
Gigi
1e7be50e35 refactor: change nostrverse icon from fa-globe to fa-network-wired
- Avoid icon conflict with web bookmarks which use fa-globe
- fa-network-wired better represents the nostr network/nostrverse concept
2025-10-06 20:31:47 +01:00
Gigi
1a7a8367a0 feat: add support for web bookmarks (NIP-B0, kind:39701)
- Update bookmarkService to fetch kind:39701 events
- Add processing logic for web bookmark events in bookmarkProcessing
- Update bookmark deduplication to handle web bookmarks
- Add 'web' type to IndividualBookmark interface
- Implement distinct icon (fa-bookmark + fa-globe) for web bookmarks
- Update CompactView and CardView to display web bookmark icon
- Add web-bookmarks rule documentation
2025-10-06 20:30:53 +01:00
Gigi
1f9dbf576c fix: load settings from local cache first to eliminate FOUT
- Check eventStore for cached settings before querying relays
- This eliminates the 5-second timeout on every page load
- Still fetch from relays in background to sync updates
- Fixes flash of unstyled text (FOUT) when custom fonts are set
2025-10-06 20:24:28 +01:00
Gigi
630c7ef0a4 feat: add comprehensive logging to settings service
- Add debug logs for settings loading from nostr
- Log when settings are found, missing, or timeout
- Add logging for settings save operations
- Track settings event publishing to relays

This will help diagnose why custom fonts/settings aren't being applied.
2025-10-06 20:20:41 +01:00
Gigi
b01293aa20 fix: ensure fonts are fully loaded before applying styles
- Convert loadFont to async function that returns a Promise
- Use Font Loading API to wait for fonts to be actually ready
- Add comprehensive logging for font loading stages
- Wait for font loading in useSettings before applying CSS variables
- Update Settings component to handle async font loading
- Prevents FOUT (Flash of Unstyled Text) by ensuring fonts are ready
- Fixes timing issue where custom fonts weren't being applied consistently

This ensures custom fonts are fully loaded and ready before being applied,
eliminating the race condition where content would render with system fonts
before custom fonts were available.
2025-10-06 20:04:11 +01:00
Gigi
d9db10fd70 fix: improve highlight rendering pipeline with comprehensive debugging
- Add extensive logging to track highlight rendering through entire pipeline
- Fix markdown rendering to wait for HTML conversion before displaying
- Prevent fallback to non-highlighted markdown during initial render
- Add debugging to URL filtering to identify matching issues
- Add logging to highlight application to track matching success/failures
- Ensure highlights are always applied when content is ready
- Show mini loading spinner while markdown is being converted

This will help diagnose and fix cases where highlights aren't showing up.
2025-10-06 19:56:20 +01:00
Gigi
872d38c7f3 feat: implement optimistic updates for highlight creation
- Update createHighlight to return the signed NostrEvent
- Add eventToHighlight helper to convert events to Highlight objects
- Immediately add new highlights to UI without fetching from relays
- Remove handleHighlightCreated refresh logic (no longer needed)
- Improves UX with instant feedback when creating highlights
2025-10-06 19:51:14 +01:00
Gigi
06c3c1ff20 feat: enable highlight creation from external URLs
- Update createHighlight service to accept both NostrEvent and URL string as source
- Modify Bookmarks component to support highlighting on /r/* paths
- Add fetchHighlightsForUrl import for refreshing URL-based highlights
- Extract context from reader content (markdown/html) for external URLs
- Automatically use 'r' tag for external URLs via HighlightBlueprint
2025-10-06 19:43:27 +01:00
Gigi
107d6757bd feat: add routing support for external URLs
- Add /r/* route in App.tsx for external URL content
- Create useExternalUrlLoader hook to load external web content
- Add fetchHighlightsForUrl service to fetch highlights by URL using 'r' tag
- Update Bookmarks component to handle both nostr-native (naddr) and external URLs
- Support two URL patterns: /a/naddr... for nostr content, /r/https://... for external URLs
2025-10-06 19:22:18 +01:00
Gigi
89bd9f631a feat: add context to highlights (previous and next sentences) 2025-10-06 07:41:19 +01:00
Gigi
beeb296d3b feat: add Boris branding to highlight alt tag 2025-10-06 07:39:45 +01:00
Gigi
0e992ae814 fix: update local relay port to 10547 2025-10-05 23:42:44 +01:00
Gigi
8b023af6a0 refactor: simplify to single RELAYS constant (DRY) 2025-10-05 23:41:27 +01:00
Gigi
6e2f1102f7 feat: add local relay support and centralize relay configuration 2025-10-05 23:38:56 +01:00
Gigi
7de8c49b01 chore: bump version to 0.2.1 2025-10-05 23:35:23 +01:00
Gigi
c3aece1722 fix: properly await account loading from localStorage on refresh 2025-10-05 23:34:33 +01:00
Gigi
7a4cb77aa3 refactor: remove dedicated login page, handle login through main UI 2025-10-05 23:32:52 +01:00
Gigi
9065501043 fix: add protected routes to prevent logout on page refresh 2025-10-05 23:31:30 +01:00
Gigi
c9ace72d4d fix: use undo icon for reset to defaults button 2025-10-05 23:29:33 +01:00
Gigi
be6ad79f60 docs: add vision section and explain three-level highlight system 2025-10-05 23:28:50 +01:00
Gigi
0473ba71fb fix: update color palette to include default friends/nostrverse colors 2025-10-05 23:28:18 +01:00
Gigi
7e575ea617 feat: add reset to defaults button in settings 2025-10-05 23:27:43 +01:00
Gigi
c3a2dd5603 feat: load and apply settings upon login 2025-10-05 23:23:59 +01:00
11033 changed files with 31612 additions and 1655045 deletions

View File

@@ -5,4 +5,4 @@ alwaysApply: false
We use FontAwesome. If you can use a fa-icon (instead of text) use a fa-icon. Always strive to keep the UI modern, beautiful, and minimalistic. Shy away from using too many colors, borders, glow, and animations.
Never write "Loading" - always show a spinner, and just a spinner.
Never write "Loading" - always show a loading placeholder (or a loading spinner, when appropriate).

View File

@@ -0,0 +1,8 @@
---
description: anything related to UI/UX
alwaysApply: false
---
This is a mobile-first application. All UI elements should be designed with that in mind. The application should work well on small screens, including older smartphones. The UX should be immaculate on mobile, even when in flight mode. (We use local caches and local relays, so that app works offline too.)
Let's not show too many error messages, and more importantly: let's not make them red. Nothing is ever this tragic.

View File

@@ -0,0 +1,10 @@
---
description: anything to do with "web bookmarks" aka NIP-B0
alwaysApply: false
---
The app also supports web bookmarks (`kind:39701`) which are distinct from public/private bookmarks as defined in NIP-51.
See NIP-B0 for details:
- https://github.com/nostr-protocol/nips/blob/master/B0.md

View File

@@ -0,0 +1,20 @@
---
description: Specification for zaps and zaps splits
alwaysApply: false
---
When we create highlights, we want to add `zap` tags to the event, to allow for value splits between the highlighter/curator and the author (or authors).
`zap` tags are defined in Appendix G of NIP-57:
- https://github.com/nostr-protocol/nips/blob/master/57.md
More on `zap` tags here:
- https://nostrbook.dev/tags/zap
Note that nostr-native content might have `zap` tags already, which can be seen as the "author group" of e.g. the long-form article (writer, editor, designer, etc). We should respect these `zap` tags and include them into our "zap splits" appropriately.
Example: if our zap-split setting is 50/50, and the nostr-native blog post has two authors, our zap splits should be as follows:
- Highlighter: 50%
- Author1: 25%
- Author2: 25%

2
.env Normal file
View File

@@ -0,0 +1,2 @@
# Default article to display on app load
VITE_DEFAULT_ARTICLE_NADDR=naddr1qvzqqqr4gupzqmjxss3dld622uu8q25gywum9qtg4w4cv4064jmg20xsac2aam5nqqxnzd3cxqmrzv3exgmr2wfesgsmew

124
.gitignore vendored
View File

@@ -1,121 +1,15 @@
# Dependencies
node_modules/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
# Build outputs
dist/
build/
.vite/
.vite/deps/
# Environment variables
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
# IDE and editor files
.vscode/
.idea/
*.swp
*.swo
*~
# OS generated files
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db
# Logs
logs
*.log
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Coverage directory used by tools like istanbul
coverage/
*.lcov
# nyc test coverage
.nyc_output
# Dependency directories
jspm_packages/
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
# Nuxt.js build / generate output
.nuxt
# Build output
dist
# Storybook build outputs
.out
.storybook-out
# Misc
*.log
.DS_Store
# Temporary folders
tmp/
temp/
# Reference Projects
applesauce
primal-web-app
Amber
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
# TypeScript cache
*.tsbuildinfo
# Vite
.vite/
# Local development
.env.local
.env.development.local
.env.test.local
.env.production.local
# applesauce examples
applesauce/

155
Amber.md Normal file
View File

@@ -0,0 +1,155 @@
## Boris ↔ Amber bunker: current findings
- **Environment**
- Client: Boris (web) using `applesauce` stack (`NostrConnectSigner`, `RelayPool`).
- Bunker: Amber (mobile).
- We restored a `nostr-connect` account from localStorage and re-wired the signer to the app `RelayPool` before use.
## What we changed client-side
- **Signer wiring**
- Bound `NostrConnectSigner.subscriptionMethod/publishMethod` to the app `RelayPool` at startup.
- After deserialization, recreated the signer with pool context and merged its relays with app `RELAYS` (includes local relays).
- Opened the signer subscription and performed a guarded `connect()` with default permissions including `nip04_encrypt/decrypt` and `nip44_encrypt/decrypt`.
- **Account queue disabling (CRITICAL)**
- `applesauce-accounts` `BaseAccount` queues requests by default - each request waits for the previous one to complete before being sent.
- This caused batch decrypt operations to hang: first request would timeout waiting for user interaction, blocking all subsequent requests in the queue.
- **Solution**: Set `accounts.disableQueue = true` globally on the `AccountManager` in `App.tsx` during initialization. This applies to all accounts.
- Without this, Amber never sees decrypt requests because they're stuck in the account's internal queue.
- Reference: https://hzrd149.github.io/applesauce/typedoc/classes/applesauce-accounts.BaseAccount.html#disablequeue
- **Probes and timeouts**
- Initial probe tried `decrypt('invalid-ciphertext')` → timed out.
- Switched to roundtrip probes: `encrypt(self, ... )` then `decrypt(self, cipher)` for both nip-44 and nip-04.
- Increased probe timeout from 3s → 10s; increased bookmark decrypt timeout from 15s → 30s.
- **Logging**
- Added logs for publish/subscribe and parsed the NIP-46 request content length.
- Confirmed NIP46 request events are kind `24133` with a single `p` tag (expected). The method is inside the encrypted content, so it prints as `method: undefined` (expected).
## Evidence from logs (client)
```
[bunker] ✅ Wired NostrConnectSigner to RelayPool publish/subscription
[bunker] 🔗 Signer relays merged with app RELAYS: (19) [...]
[bunker] subscribe via signer: { relays: [...], filters: [...] }
[bunker] ✅ Signer subscription opened
[bunker] publish via signer: { relays: [...], kind: 24133, tags: [['p', <remote>]], contentLength: 260|304|54704 }
[bunker] 🔎 Probe nip44 roundtrip (encrypt→decrypt)… → probe timeout after 10000ms
[bunker] 🔎 Probe nip04 roundtrip (encrypt→decrypt)… → probe timeout after 10000ms
bookmarkProcessing.ts: ❌ nip44.decrypt failed: Decrypt timeout after 30000ms
bookmarkProcessing.ts: ❌ nip04.decrypt failed: Decrypt timeout after 30000ms
```
Notes:
- Final signer status shows `listening: true`, `isConnected: true`, and requests are published to 19 relays (includes Ambers).
## Evidence from Amber (device)
- Activity screen shows multiple entries for: “Encrypt data using nip 4” and “Encrypt data using nip 44” with green checkmarks.
- No entries for “Decrypt data using nip 4” or “Decrypt data using nip 44”.
## Interpretation
- Transport and publish paths are working: Boris is publishing NIP46 requests (kind 24133) and Amber receives them (ENCRYPT activity visible).
- The persistent failure is specific to DECRYPT handling: Amber does not show any DECRYPT activity and Boris receives no decrypt responses within 1030s windows.
- Client-side wiring is likely correct (subscription open, permissions requested, relays merged). The remaining issue appears provider-side in Ambers NIP46 decrypt handling or permission gating.
## Repro steps (quick)
1) Revoke Boris in Amber.
2) Reconnect with a fresh bunker URI; approve signing and both encrypt/decrypt scopes for nip04 and nip44.
3) Keep Amber unlocked and foregrounded.
4) Reload Boris; observe:
- Logs showing `publish via signer` for kind 24133.
- In Amber, activity should include “Decrypt data using nip 4/44”.
If DECRYPT entries still dont appear:
- This points to Ambers NIP46 provider not executing/authorizing `nip04_decrypt`/`nip44_decrypt` methods, or not publishing responses.
## Suggestions for Amber-side debugging
- Verify permission gating allows `nip04_decrypt` and `nip44_decrypt` (not just encrypt).
- Confirm the provider recognizes NIP46 methods `nip04_decrypt` and `nip44_decrypt` in the decrypted payload and routes them to decrypt routines.
- Ensure the response event is published back to the same relays and correctly addressed to the client (`p` tag set and content encrypted back to client pubkey).
- Add activity logging for “Decrypt …” attempts and failures to surface denial/exception states.
## Performance improvements (post-debugging)
### Non-blocking publish wiring
- **Problem**: Awaiting `pool.publish()` completion blocks until all relay sends finish (can take 30s+ with timeouts).
- **Solution**: Wrapped `NostrConnectSigner.publishMethod` at app startup to fire-and-forget publish Observable/Promise; responses still arrive via signer subscription.
- **Result**: Encrypt/decrypt operations complete in <2s as seen in `/debug` page (NIP-44: ~900ms enc, ~700ms dec; NIP-04: ~1s enc, ~2s dec).
### Bookmark decryption optimization
- **Problem #1**: Sequential decrypt of encrypted bookmark events blocks UI and takes long with multiple events.
- **Problem #2**: 30-second timeouts on `nip44.decrypt` meant waiting 30s per event if bunker didn't support nip44.
- **Problem #3**: Account request queue blocked all decrypt requests until first one completed (waiting for user interaction).
- **Solution**:
- Removed all artificial timeouts - let decrypt fail naturally like debug page does.
- Added smart encryption detection (NIP-04 has `?iv=`, NIP-44 doesn't) to try the right method first.
- **Disabled account queue globally** (`accounts.disableQueue = true`) in `App.tsx` so all requests are sent immediately.
- Process sequentially (removed concurrent `mapWithConcurrency` hack).
- **Result**: Bookmark decryption is near-instant, limited only by bunker response time and user approval speed.
## Amethyst-style bookmarks (kind:30001)
**Important**: Amethyst bookmarks are stored in a **SINGLE** `kind:30001` event with d-tag `"bookmark"` that contains BOTH public AND private bookmarks in different parts of the event.
### Event structure:
- **Event kind**: `30001` (NIP-51 bookmark set)
- **d-tag**: `"bookmark"` (identifies this as the Amethyst bookmark list)
- **Public bookmarks**: Stored in event `tags` (e.g., `["e", "..."]`, `["a", "..."]`)
- **Private bookmarks**: Stored in encrypted `content` field (NIP-04 or NIP-44)
### Example event:
```json
{
"kind": 30001,
"tags": [
["d", "bookmark"], // Identifies this as Amethyst bookmarks
["e", "102a2fe..."], // Public bookmark (76 total)
["a", "30023:..."] // Public bookmark
],
"content": "lvOfl7Qb...?iv=5KzDXv09..." // NIP-04 encrypted (416 private bookmarks)
}
```
### Processing:
When this single event is processed:
1. **Public tags** 76 bookmark items with `sourceKind: 30001, isPrivate: false, setName: "bookmark"`
2. **Encrypted content** 416 bookmark items with `sourceKind: 30001, isPrivate: true, setName: "bookmark"`
3. Total: 492 bookmarks from one event
### Encryption detection:
- The encrypted `content` field contains a JSON array of private bookmark tags
- `Helpers.hasHiddenContent()` from `applesauce-core` only detects **NIP-44** encrypted content
- **NIP-04** encrypted content must be detected explicitly by checking for `?iv=` in the content string
- Both detection methods are needed in:
1. **Display logic** (`Debug.tsx` - `hasEncryptedContent()`) - to show padlock emoji and decrypt button
2. **Decryption logic** (`bookmarkProcessing.ts`) - to schedule decrypt jobs
### Grouping:
In the UI, these are separated into two groups:
- **Amethyst Lists**: `sourceKind === 30001 && !isPrivate && setName === 'bookmark'` (public items)
- **Amethyst Private**: `sourceKind === 30001 && isPrivate && setName === 'bookmark'` (private items)
Both groups come from the same event, separated by whether they were in public tags or encrypted content.
### Why this matters:
This dual-storage format (public + private in one event) is why we need explicit NIP-04 detection. Without it, `Helpers.hasHiddenContent()` returns `false` and the encrypted content is never decrypted, resulting in 0 private bookmarks despite having encrypted data.
## Current conclusion
- Client is configured and publishing requests correctly; encryption proves endtoend path is alive.
- Non-blocking publish keeps operations fast (~1-2s for encrypt/decrypt).
- **Account queue is GLOBALLY DISABLED** - this was the primary cause of hangs/timeouts.
- Smart encryption detection (both NIP-04 and NIP-44) and no artificial timeouts make operations instant.
- Sequential processing is cleaner and more predictable than concurrent hacks.
- Relay queries now trust EOSE signals instead of arbitrary timeouts, completing in 1-2s instead of 6s.
- The missing DECRYPT activity in Amber was partially due to requests never being sent (stuck in queue). With queue disabled globally, Amber receives all decrypt requests immediately.
- **Amethyst-style bookmarks** require explicit NIP-04 detection (`?iv=` check) since `Helpers.hasHiddenContent()` only detects NIP-44.

1979
CHANGELOG.md Normal file

File diff suppressed because it is too large Load Diff

89
FEATURES.md Normal file
View File

@@ -0,0 +1,89 @@
# Boris Features
## Overview
- **Purpose**: A calm, fast, Nostrfirst reader that turns your bookmarks into a focused reading app.
- **Layout**: Threepane interface — bookmarks (left), reader (center), highlights (right). Collapsible sidebars.
- **Content**: Renders both Nostr longform posts (kind:30023) and regular web URLs.
- **Social layer**: Highlights shown by level — mine, friends, nostrverse — each with its own color and visibility toggle.
## Reader Experience
- **Distractionfree view**: Clean typography, optional hero image, summary, and published date.
- **Reading time**: Displays estimated reading time for text or duration for supported videos.
- **Progress**: Reading progress indicator with completion state.
- **Menus**: Quick actions to open, share, or copy links (for both Nostr and web content).
- **Performance**: Lightweight fetching and caching for speed; skeleton loaders to avoid empty flashes.
## Highlights (NIP84)
- **Levels**: Mine, friends, nostrverse; toggle per level; colors configurable in settings.
- **Interactions**: Click a highlight to scroll to its position; count indicator in the header.
- **Creation**: Select text and use the floating highlighter button to publish a highlight.
- **Attribution**: Automatically tags article authors for Nostr posts so they can see highlights.
## Zap Splits (NIP57)
- **Configurable splits**: Weightbased sliders for highlighter, author(s), and Boris (defaults 50/50/2.1).
- **Presets**: Quick buttons for common split configurations.
- **Respect source**: If the source article has zap tags, author weights are proportionally preserved.
## Bookmarks & Reading List (NIP51 + Web)
- **Ingestion**: Collects list bookmarks and items from kinds 10003/30003/30001.
- **Web bookmarks**: Supports NIPB0 (kind:39701) for standalone URL bookmarks.
- **Add Bookmark**: Modal with auto title/description extraction and keywords/tags suggestion (adds “boris” when helpful).
- **Views**: Reading list in compact, cards, or large preview modes; quick toggles to switch.
- **Archive**: “Read” items appear in your archive; can mark articles/web pages as read.
## Explore & Profiles
- **Explore**: Discover friends' highlights and writings, plus a "nostrverse" feed.
- **Filters**: Visibility toggles (mine, friends, nostrverse) apply to Explore highlights.
- **Profiles**: View your own (`/me`) or other users (`/p/:npub`) with tabs for Highlights, Bookmarks, Archive, and Writings.
## Support
- **Supporter page**: Displays avatars of users who zapped Boris (kind:9735 receipts).
- **Thresholds**: Shows supporters who sent ≥ 2100 sats; whales (≥ 69420 sats) get special styling with a bolt badge.
- **Profile integration**: Fetches and displays profile pictures and names for all supporters.
- **Stats**: Total supporter count and zap count displayed at the bottom.
## Video
- **Embedded player**: Plays supported videos (e.g., YouTube) inline with duration display.
- **Metadata**: Fetches YouTube title/description/transcript when available.
- **Deep links**: Open in native apps via platformspecific URL schemes.
## Settings (NIP78 Application Data)
- **Theme**: System/light/dark with color variants (dark: black/midnight/charcoal; light: paperwhite/sepia/ivory).
- **Reading**: Font family (preloaded), font size, highlight style (marker/underline), perlevel colors.
- **Layout & startup**: Default view modes, autocollapse preferences, show/hide highlights.
- **Zap Splits**: Weight sliders and presets for NIP57 splits.
- **Offline/Flight Mode**: Local image cache with size limit and clear controls; “use local relay as cache”; rebroadcast preferences.
- **Relays**: Relay overview and status in Settings; educational links.
- **PWA**: Install prompt when available.
## Offline, PWA, and Sync
- **PWA**: Installable; service worker registered; periodic update checks with inapp toast.
- **Flight Mode**: Operates with local relays only; highlights created offline are stored locally and synced later.
- **Relay indicator**: Floating status indicator shows Connecting/Offline/Flight Mode and connected counts.
## Relays & Accounts
- **Applesauce stack**: Accounts, event store, relay pool, and blueprints power Nostr interactions.
- **Multirelay**: Grouped connections with keepalive subscription; local+remote partitioning for fast queries.
- **Persistence**: Accounts restored from local storage; settings saved to NIP78 and watched for updates.
## Privacy
- **Identity**: No email or new account; uses your existing Nostr signer/identity.
- **Data**: Bookmarks and highlights live on Nostr; reading/rendering happens locally in your browser.
## Conveniences
- **Share/copy**: Oneclick copy or share for articles and videos.
- **Open on Nostr**: Deep links to portals and `nostr:` schemes for longform articles.
- **Mobile UX**: Floating open buttons for Bookmarks/Highlights, focus trapping, and backdrop controls.

22
LICENSE Normal file
View File

@@ -0,0 +1,22 @@
MIT License
Copyright (c) 2025 Gigi
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -2,13 +2,40 @@
Your reading list for the Nostr world.
Boris turns your Nostr bookmarks into a calm, fast, and focused reading experience. Connect your Nostr account and youll get a clean threepane reader: bookmarks on the left, the article in the middle, and highlights on the right.
Boris turns your Nostr bookmarks into a calm, fast, and focused reading experience. Connect your Nostr account and you'll get a clean threepane reader: bookmarks on the left, the article in the middle, and highlights on the right.
## Live
- App: [https://read.withboris.com/](https://read.withboris.com/)
## The Vision
When I wrote "Purple Text, Orange Highlights" 2.5 years ago, I had a certain interface in mind that would allow the reader to curate, discover, highlight, and provide value to writers and other readers alike. Boris is my attempt to build this interface.
Boris has three "levels" of highlights for each article:
- user = yellow
- friends = orange
- nostrverse = purple
In case it's not self-explanatory:
- **your highlights** = highlights that the logged-in npub made
- **friends** = highlights that your friends made, i.e. highlights of the npubs that the logged-in user follows
- **nostrverse** = all the highlights we can find on all the relays we're connected to
The user can toggle hide/show any of these "levels".
In addition to rendering articles from nostr and the legacy web, Boris can act as a "read it later" app, thanks to the power of nostr bookmarks.
If you bookmark something on nostr, Boris will show it in the bookmarks bar. If said something contains a URL, Boris will extract and render it in a distraction-free and reader-friendly way.
## What Boris does
- Collects your saved links from Nostr and shows them as a tidy reading list
- Opens articles in a distractionfree reader with clear typography
- Shows community highlights layered on the article (yours, friends, everyone)
- Splits zaps between you and the author(s) when you highlight
- Lets you collapse sidebars anytime for fullfocus reading
- Remembers simple preferences like view mode, fonts, and highlight style

188
TAILWIND_MIGRATION.md Normal file
View File

@@ -0,0 +1,188 @@
# Tailwind CSS Migration Status
## ✅ Completed (Core Infrastructure)
### Phase 1: Setup & Foundation
- [x] Install Tailwind CSS with PostCSS and Autoprefixer
- [x] Configure `tailwind.config.js` with content globs and custom keyframes
- [x] Create `src/styles/tailwind.css` with base/components/utilities
- [x] Import Tailwind before existing CSS in `main.tsx`
- [x] Enable Tailwind preflight (CSS reset)
### Phase 2: Base Styles Reconciliation
- [x] Add CSS variables for user-settable theme colors
- `--highlight-color-mine`, `--highlight-color-friends`, `--highlight-color-nostrverse`
- `--reading-font`, `--reading-font-size`
- [x] Simplify `global.css` to work with Tailwind preflight
- [x] Remove redundant base styles handled by Tailwind
- [x] Keep app-specific overrides (mobile sidebar lock, loading states)
### Phase 3: Layout System Refactor ⭐ **CRITICAL FIX**
- [x] Switch from pane-scrolling to document-scrolling
- [x] Make sidebars sticky on desktop (`position: sticky`)
- [x] Update `app.css` to remove fixed container heights
- [x] Update `ThreePaneLayout.tsx` to use window scroll
- [x] Fix reading position tracking to work with document scroll
- [x] Maintain mobile overlay behavior
### Phase 4: Component Migrations
- [x] **ReadingProgressIndicator**: Full Tailwind conversion
- Removed 80+ lines of CSS
- Added shimmer animation to Tailwind config
- Z-index layering maintained (1102)
- [x] **Mobile UI Elements**: Tailwind utilities
- Mobile hamburger button
- Mobile highlights button
- Mobile backdrop
- Removed 60+ lines of CSS
- [x] **App Container**: Tailwind utilities
- Responsive padding (p-0 md:p-4)
- Min-height viewport support
## 📊 Impact & Metrics
### Lines of CSS Removed
- `global.css`: ~50 lines removed
- `reader.css`: ~80 lines removed (progress indicator)
- `app.css`: ~30 lines removed (mobile buttons/backdrop)
- `sidebar.css`: ~30 lines removed (mobile hamburger)
- **Total**: ~190 lines removed
### Key Achievements
1. **Fixed Core Issue**: Reading position tracking now works correctly with document scroll
2. **Tailwind Integration**: Fully functional with preflight enabled
3. **No Breaking Changes**: All existing functionality preserved
4. **Type Safety**: TypeScript checks passing
5. **Lint Clean**: ESLint checks passing
6. **Responsive**: Mobile/tablet/desktop layouts working
## 🔄 Remaining Work (Incremental)
The following migrations are **optional enhancements** that can be done as components are touched:
### High-Value Components
- [ ] **ContentPanel** - Large component, high impact
- Reader header, meta info, loading states
- Mark as read button
- Article/video menus
- [ ] **BookmarkList & BookmarkItem** - Core UI
- Card layouts (compact/cards/large views)
- Bookmark metadata display
- Interactive states
- [ ] **HighlightsPanel** - Feature-rich
- Header with toggles
- Highlight items
- Level-based styling
- [ ] **Settings Components** - Forms & controls
- Color pickers
- Font selectors
- Toggle switches
- Sliders
### CSS Files to Prune
- `src/index.css` - Contains many inline bookmark/highlight styles (~3000+ lines)
- `src/styles/components/cards.css` - Bookmark card styles
- `src/styles/components/modals.css` - Modal dialogs
- `src/styles/layout/highlights.css` - Highlight panel layout
## 🎯 Migration Strategy
### For New Components
Use Tailwind utilities from the start. Reference:
```tsx
// Good: Tailwind utilities
<div className="flex items-center gap-2 p-4 bg-gray-800 rounded-lg">
// Avoid: New CSS classes
<div className="custom-component">
```
### For Existing Components
Migrate incrementally when touching files:
1. Replace layout utilities (flex, grid, spacing, sizing)
2. Replace color/background utilities
3. Replace typography utilities
4. Replace responsive variants
5. Remove old CSS rules
6. Keep file under 210 lines
### CSS Variable Usage
Dynamic values should still use CSS variables or inline styles:
```tsx
// User-settable colors
style={{ backgroundColor: settings.highlightColorMine }}
// Or reference CSS variable
className="bg-[var(--highlight-color-mine)]"
```
## 📝 Technical Notes
### Z-Index Layering
- Mobile sidepanes: `z-[1001]`
- Mobile backdrop: `z-[999]`
- Progress indicator: `z-[1102]`
- Mobile buttons: `z-[900]`
- Relay status: `z-[999]`
- Modals: `z-[10000]`
### Responsive Breakpoints
- Mobile: `< 768px`
- Tablet: `768px - 1024px`
- Desktop: `> 1024px`
Use Tailwind: `md:` (768px), `lg:` (1024px)
### Safe Area Insets
Mobile notch support:
```tsx
style={{
top: 'calc(1rem + env(safe-area-inset-top))',
left: 'calc(1rem + env(safe-area-inset-left))'
}}
```
### Custom Animations
Add to `tailwind.config.js`:
```js
keyframes: {
shimmer: {
'0%': { transform: 'translateX(-100%)' },
'100%': { transform: 'translateX(100%)' },
},
}
```
## ✅ Success Criteria Met
- [x] Tailwind CSS fully integrated and functional
- [x] Document scrolling working correctly
- [x] Reading position tracking accurate
- [x] Progress indicator always visible
- [x] No TypeScript errors
- [x] No linting errors
- [x] Mobile responsiveness maintained
- [x] Theme colors (user settings) working
- [x] All existing features functional
## 🚀 Next Steps
1. **Ship It**: Current state is production-ready
2. **Incremental Migration**: Convert components as you touch them
3. **Monitor**: Watch for any CSS conflicts
4. **Cleanup**: Eventually remove unused CSS files
5. **Document**: Update component docs with Tailwind patterns
---
**Status**: ✅ **CORE MIGRATION COMPLETE**
**Date**: 2025-01-14
**Commits**: 8 conventional commits
**Lines Removed**: ~190 lines of CSS
**Breaking Changes**: None

304
api/article-og.ts Normal file
View File

@@ -0,0 +1,304 @@
import type { VercelRequest, VercelResponse } from '@vercel/node'
import { RelayPool } from 'applesauce-relay'
import { nip19 } from 'nostr-tools'
import { AddressPointer } from 'nostr-tools/nip19'
import { NostrEvent, Filter } from 'nostr-tools'
import { Helpers } from 'applesauce-core'
const { getArticleTitle, getArticleImage, getArticleSummary } = Helpers
// Relay configuration (from src/config/relays.ts)
const RELAYS = [
'wss://relay.damus.io',
'wss://nos.lol',
'wss://relay.nostr.band',
'wss://relay.dergigi.com',
'wss://wot.dergigi.com',
'wss://relay.snort.social',
'wss://relay.current.fyi',
'wss://nostr-pub.wellorder.net',
'wss://purplepag.es',
'wss://relay.primal.net'
]
type CacheEntry = {
html: string
expires: number
}
const WEEK_MS = 7 * 24 * 60 * 60 * 1000
const memoryCache = new Map<string, CacheEntry>()
function escapeHtml(text: string): string {
return text
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;')
}
function setCacheHeaders(res: VercelResponse, maxAge: number = 86400): void {
res.setHeader('Cache-Control', `public, max-age=${maxAge}, s-maxage=604800`)
res.setHeader('Content-Type', 'text/html; charset=utf-8')
}
interface ArticleMetadata {
title: string
summary: string
image: string
author: string
published?: number
}
async function fetchEventsFromRelays(
relayPool: RelayPool,
relayUrls: string[],
filter: Filter,
timeoutMs: number
): Promise<NostrEvent[]> {
const events: NostrEvent[] = []
await new Promise<void>((resolve) => {
const timeout = setTimeout(() => resolve(), timeoutMs)
// `request` emits NostrEvent objects directly
relayPool.request(relayUrls, filter).subscribe({
next: (event) => {
events.push(event)
},
error: () => resolve(),
complete: () => {
clearTimeout(timeout)
resolve()
}
})
})
// Sort by created_at and return most recent first
return events.sort((a, b) => b.created_at - a.created_at)
}
async function fetchArticleMetadata(naddr: string): Promise<ArticleMetadata | null> {
const relayPool = new RelayPool()
try {
// Decode naddr
const decoded = nip19.decode(naddr)
if (decoded.type !== 'naddr') {
return null
}
const pointer = decoded.data as AddressPointer
// Determine relay URLs
const relayUrls = pointer.relays && pointer.relays.length > 0 ? pointer.relays : RELAYS
// Fetch article and profile in parallel
const [articleEvents, profileEvents] = await Promise.all([
fetchEventsFromRelays(relayPool, relayUrls, {
kinds: [pointer.kind],
authors: [pointer.pubkey],
'#d': [pointer.identifier || '']
}, 5000),
fetchEventsFromRelays(relayPool, relayUrls, {
kinds: [0],
authors: [pointer.pubkey]
}, 3000)
])
if (articleEvents.length === 0) {
return null
}
const article = articleEvents[0]
// Extract article metadata
const title = getArticleTitle(article) || 'Untitled Article'
const summary = getArticleSummary(article) || 'Read this article on Boris'
const image = getArticleImage(article) || '/boris-social-1200.png'
// Extract author name from profile
let authorName = pointer.pubkey.slice(0, 8) + '...'
if (profileEvents.length > 0) {
try {
const profileData = JSON.parse(profileEvents[0].content)
authorName = profileData.display_name || profileData.name || authorName
} catch {
// Use fallback
}
}
return {
title,
summary,
image,
author: authorName,
published: article.created_at
}
} catch (err) {
console.error('Failed to fetch article metadata:', err)
return null
} finally {
// No explicit close needed; pool manages connections internally
}
}
function generateHtml(naddr: string, meta: ArticleMetadata | null): string {
const baseUrl = 'https://read.withboris.com'
const articleUrl = `${baseUrl}/a/${naddr}`
const title = meta?.title || 'Boris Nostr Bookmarks'
const description = meta?.summary || 'Your reading list for the Nostr world. A minimal nostr client for bookmark management with highlights.'
const image = meta?.image?.startsWith('http') ? meta.image : `${baseUrl}${meta?.image || '/boris-social-1200.png'}`
const author = meta?.author || 'Boris'
return `<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<meta name="theme-color" content="#0f172a" />
<link rel="manifest" href="/manifest.webmanifest" />
<title>${escapeHtml(title)}</title>
<meta name="description" content="${escapeHtml(description)}" />
<link rel="canonical" href="${articleUrl}" />
<!-- Open Graph / Social Media -->
<meta property="og:type" content="article" />
<meta property="og:url" content="${articleUrl}" />
<meta property="og:title" content="${escapeHtml(title)}" />
<meta property="og:description" content="${escapeHtml(description)}" />
<meta property="og:image" content="${escapeHtml(image)}" />
<meta property="og:site_name" content="Boris" />
${meta?.published ? `<meta property="article:published_time" content="${new Date(meta.published * 1000).toISOString()}" />` : ''}
<meta property="article:author" content="${escapeHtml(author)}" />
<!-- Twitter Card -->
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:url" content="${articleUrl}" />
<meta name="twitter:title" content="${escapeHtml(title)}" />
<meta name="twitter:description" content="${escapeHtml(description)}" />
<meta name="twitter:image" content="${escapeHtml(image)}" />
</head>
<body>
<noscript>
<p>Redirecting to <a href="/">Boris</a>...</p>
</noscript>
</body>
</html>`
}
function isCrawler(userAgent: string | undefined): boolean {
if (!userAgent) return false
const crawlers = [
'bot', 'crawl', 'spider', 'slurp', 'facebook', 'twitter', 'linkedin',
'whatsapp', 'telegram', 'slack', 'discord', 'preview'
]
const ua = userAgent.toLowerCase()
return crawlers.some(crawler => ua.includes(crawler))
}
export default async function handler(req: VercelRequest, res: VercelResponse) {
const naddr = (req.query.naddr as string | undefined)?.trim()
if (!naddr) {
return res.status(400).json({ error: 'Missing naddr parameter' })
}
const userAgent = req.headers['user-agent'] as string | undefined
const isCrawlerRequest = isCrawler(userAgent)
const debugEnabled = req.query.debug === '1' || req.headers['x-boris-debug'] === '1'
if (debugEnabled) {
console.log('[article-og] request', JSON.stringify({
naddr,
ua: userAgent || null,
isCrawlerRequest,
path: req.url || null
}))
res.setHeader('X-Boris-Debug', '1')
}
// If it's a regular browser (not a bot), serve HTML that loads SPA
// Use history.replaceState to set the URL before the SPA boots
if (!isCrawlerRequest) {
const articlePath = `/a/${naddr}`
// Serve a minimal HTML that sets up the URL and loads the SPA
const html = `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<link rel="icon" type="image/x-icon" href="/favicon.ico">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Boris - Loading Article...</title>
<script>
// Set the URL to the article path before SPA loads
if (window.location.pathname !== '${articlePath}') {
history.replaceState(null, '', '${articlePath}');
}
</script>
${debugEnabled ? `<script>console.debug('article-og', { mode: 'browser', naddr: '${naddr}', path: location.pathname, referrer: document.referrer });</script>` : ''}
<script>
// Redirect to index.html which will load the SPA
// The history state is already set, so SPA will see the correct URL
window.location.replace('/');
</script>
</head>
<body>
<div id="root"></div>
</body>
</html>`
res.setHeader('Content-Type', 'text/html; charset=utf-8')
res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate')
if (debugEnabled) {
console.log('[article-og] response', JSON.stringify({ mode: 'browser', naddr }))
}
return res.status(200).send(html)
}
// Check cache for bots/crawlers
const now = Date.now()
const cached = memoryCache.get(naddr)
if (cached && cached.expires > now) {
setCacheHeaders(res)
if (debugEnabled) {
console.log('[article-og] response', JSON.stringify({ mode: 'bot', naddr, cache: true }))
}
return res.status(200).send(cached.html)
}
try {
// Fetch metadata
const meta = await fetchArticleMetadata(naddr)
// Generate HTML
const html = generateHtml(naddr, meta)
// Cache the result
memoryCache.set(naddr, { html, expires: now + WEEK_MS })
// Send response
setCacheHeaders(res)
if (debugEnabled) {
console.log('[article-og] response', JSON.stringify({ mode: 'bot', naddr, cache: false }))
}
return res.status(200).send(html)
} catch (err) {
console.error('Error generating article OG HTML:', err)
// Fallback to basic HTML with SPA boot
const html = generateHtml(naddr, null)
setCacheHeaders(res, 3600)
if (debugEnabled) {
console.log('[article-og] response', JSON.stringify({ mode: 'bot-fallback', naddr }))
}
return res.status(200).send(html)
}
}

201
api/video-meta.ts Normal file
View File

@@ -0,0 +1,201 @@
import type { VercelRequest, VercelResponse } from '@vercel/node'
import { getSubtitles } from '@treeee/youtube-caption-extractor'
type Caption = { start: number; dur: number; text: string }
type Subtitle = { start: string | number; dur: string | number; text: string }
type CacheEntry = {
body: unknown
expires: number
}
type VimeoOEmbedResponse = {
title: string
description: string
author_name: string
author_url: string
provider_name: string
provider_url: string
type: string
version: string
width: number
height: number
html: string
thumbnail_url: string
thumbnail_width: number
thumbnail_height: number
}
// In-memory cache for 7 days
const WEEK_MS = 7 * 24 * 60 * 60 * 1000
const memoryCache = new Map<string, CacheEntry>()
function buildKey(videoId: string, lang: string, preferAuto?: string | string[], source?: string) {
return `${source || 'video'}|${videoId}|${lang}|${preferAuto ? 'auto' : 'manual'}`
}
function ok(res: VercelResponse, data: unknown) {
res.setHeader('Cache-Control', 'public, max-age=86400, s-maxage=604800') // client: 1d, CDN: 7d
return res.status(200).json(data)
}
function bad(res: VercelResponse, code: number, message: string) {
return res.status(code).json({ error: message })
}
function extractVideoId(url: string): { id: string; source: 'youtube' | 'vimeo' } | null {
// YouTube patterns
const youtubePatterns = [
/(?:youtube\.com\/watch\?v=|youtu\.be\/|youtube\.com\/embed\/)([^&\n?#]+)/,
/youtube\.com\/v\/([^&\n?#]+)/
]
for (const pattern of youtubePatterns) {
const match = url.match(pattern)
if (match) {
return { id: match[1], source: 'youtube' }
}
}
// Vimeo patterns
const vimeoPatterns = [
/vimeo\.com\/(\d+)/,
/player\.vimeo\.com\/video\/(\d+)/
]
for (const pattern of vimeoPatterns) {
const match = url.match(pattern)
if (match) {
return { id: match[1], source: 'vimeo' }
}
}
return null
}
async function pickCaptions(videoID: string, preferredLangs: string[], manualFirst: boolean): Promise<{ caps: Caption[]; lang: string; isAuto: boolean } | null> {
for (const lang of preferredLangs) {
try {
const caps = await getSubtitles({ videoID, lang })
if (Array.isArray(caps) && caps.length > 0) {
// Convert the returned subtitles to our Caption format
const convertedCaps: Caption[] = caps.map((cap: Subtitle) => ({
start: typeof cap.start === 'string' ? parseFloat(cap.start) : cap.start,
dur: typeof cap.dur === 'string' ? parseFloat(cap.dur) : cap.dur,
text: cap.text
}))
return { caps: convertedCaps, lang, isAuto: !manualFirst }
}
} catch {
// try next
}
}
return null
}
async function getVimeoMetadata(videoId: string): Promise<{ title: string; description: string }> {
const vimeoUrl = `https://vimeo.com/${videoId}`
const oembedUrl = `https://vimeo.com/api/oembed.json?url=${encodeURIComponent(vimeoUrl)}`
const response = await fetch(oembedUrl)
if (!response.ok) {
throw new Error(`Vimeo oEmbed API returned ${response.status}`)
}
const data: VimeoOEmbedResponse = await response.json()
return {
title: data.title || '',
description: data.description || ''
}
}
export default async function handler(req: VercelRequest, res: VercelResponse) {
const url = (req.query.url as string | undefined)?.trim()
const videoId = (req.query.videoId as string | undefined)?.trim()
if (!url && !videoId) {
return bad(res, 400, 'Missing url or videoId parameter')
}
// Extract video info from URL or use provided videoId
let videoInfo: { id: string; source: 'youtube' | 'vimeo' }
if (url) {
const extracted = extractVideoId(url)
if (!extracted) {
return bad(res, 400, 'Unsupported video URL. Only YouTube and Vimeo are supported.')
}
videoInfo = extracted
} else {
// If only videoId is provided, assume YouTube for backward compatibility
videoInfo = { id: videoId!, source: 'youtube' }
}
const lang = ((req.query.lang as string | undefined) || 'en').toLowerCase()
const uiLocale = (req.headers['x-ui-locale'] as string | undefined)?.toLowerCase()
const preferAuto = req.query.preferAuto === 'true'
const cacheKey = buildKey(videoInfo.id, lang, preferAuto ? 'auto' : undefined, videoInfo.source)
const now = Date.now()
const cached = memoryCache.get(cacheKey)
if (cached && cached.expires > now) {
return ok(res, cached.body)
}
try {
if (videoInfo.source === 'youtube') {
// YouTube handling
// Note: getVideoDetails doesn't exist in the library, so we use a simplified approach
const title = ''
const description = ''
// Language order: manual en -> uiLocale -> lang -> any manual, then auto with same order
const langs: string[] = Array.from(new Set(['en', uiLocale, lang].filter(Boolean) as string[]))
let selected = null as null | { caps: Caption[]; lang: string; isAuto: boolean }
// Manual first
selected = await pickCaptions(videoInfo.id, langs, true)
if (!selected) {
// Try auto
selected = await pickCaptions(videoInfo.id, langs, false)
}
const captions = selected?.caps || []
const transcript = captions.map(c => c.text).join(' ').trim()
const response = {
title,
description,
captions,
transcript,
lang: selected?.lang || lang,
isAuto: selected?.isAuto || false,
source: 'youtube'
}
memoryCache.set(cacheKey, { body: response, expires: now + WEEK_MS })
return ok(res, response)
} else if (videoInfo.source === 'vimeo') {
// Vimeo handling
const { title, description } = await getVimeoMetadata(videoInfo.id)
const response = {
title,
description,
captions: [], // Vimeo doesn't provide captions through oEmbed API
transcript: '', // No transcript available
lang: 'en', // Default language
isAuto: false, // Not applicable for Vimeo
source: 'vimeo'
}
memoryCache.set(cacheKey, { body: response, expires: now + WEEK_MS })
return ok(res, response)
} else {
return bad(res, 400, 'Unsupported video source')
}
} catch (e) {
return bad(res, 500, `Failed to fetch ${videoInfo.source} metadata`)
}
}

93
api/vimeo-meta.ts Normal file
View File

@@ -0,0 +1,93 @@
import type { VercelRequest, VercelResponse } from '@vercel/node'
type CacheEntry = {
body: unknown
expires: number
}
type VimeoOEmbedResponse = {
title: string
description: string
author_name: string
author_url: string
provider_name: string
provider_url: string
type: string
version: string
width: number
height: number
html: string
thumbnail_url: string
thumbnail_width: number
thumbnail_height: number
}
// In-memory cache for 7 days
const WEEK_MS = 7 * 24 * 60 * 60 * 1000
const memoryCache = new Map<string, CacheEntry>()
function buildKey(videoId: string) {
return `vimeo|${videoId}`
}
function ok(res: VercelResponse, data: unknown) {
res.setHeader('Cache-Control', 'public, max-age=86400, s-maxage=604800') // client: 1d, CDN: 7d
return res.status(200).json(data)
}
function bad(res: VercelResponse, code: number, message: string) {
return res.status(code).json({ error: message })
}
async function getVimeoMetadata(videoId: string): Promise<{ title: string; description: string }> {
const vimeoUrl = `https://vimeo.com/${videoId}`
const oembedUrl = `https://vimeo.com/api/oembed.json?url=${encodeURIComponent(vimeoUrl)}`
const response = await fetch(oembedUrl)
if (!response.ok) {
throw new Error(`Vimeo oEmbed API returned ${response.status}`)
}
const data: VimeoOEmbedResponse = await response.json()
return {
title: data.title || '',
description: data.description || ''
}
}
export default async function handler(req: VercelRequest, res: VercelResponse) {
const videoId = (req.query.videoId as string | undefined)?.trim()
if (!videoId) return bad(res, 400, 'Missing videoId')
// Validate that videoId is a number
if (!/^\d+$/.test(videoId)) {
return bad(res, 400, 'Invalid Vimeo video ID - must be numeric')
}
const cacheKey = buildKey(videoId)
const now = Date.now()
const cached = memoryCache.get(cacheKey)
if (cached && cached.expires > now) {
return ok(res, cached.body)
}
try {
const { title, description } = await getVimeoMetadata(videoId)
const response = {
title,
description,
captions: [], // Vimeo doesn't provide captions through oEmbed API
transcript: '', // No transcript available
lang: 'en', // Default language
isAuto: false, // Not applicable for Vimeo
source: 'vimeo'
}
memoryCache.set(cacheKey, { body: response, expires: now + WEEK_MS })
return ok(res, response)
} catch (e) {
return bad(res, 500, 'Failed to fetch Vimeo metadata')
}
}

101
api/youtube-meta.ts Normal file
View File

@@ -0,0 +1,101 @@
import type { VercelRequest, VercelResponse } from '@vercel/node'
import { getSubtitles } from '@treeee/youtube-caption-extractor'
type Caption = { start: number; dur: number; text: string }
type Subtitle = { start: string | number; dur: string | number; text: string }
type CacheEntry = {
body: unknown
expires: number
}
// In-memory cache for 7 days
const WEEK_MS = 7 * 24 * 60 * 60 * 1000
const memoryCache = new Map<string, CacheEntry>()
function buildKey(videoId: string, lang: string, preferAuto?: string | string[]) {
return `${videoId}|${lang}|${preferAuto ? 'auto' : 'manual'}`
}
function ok(res: VercelResponse, data: unknown) {
res.setHeader('Cache-Control', 'public, max-age=86400, s-maxage=604800') // client: 1d, CDN: 7d
return res.status(200).json(data)
}
function bad(res: VercelResponse, code: number, message: string) {
return res.status(code).json({ error: message })
}
async function pickCaptions(videoID: string, preferredLangs: string[], manualFirst: boolean): Promise<{ caps: Caption[]; lang: string; isAuto: boolean } | null> {
for (const lang of preferredLangs) {
try {
const caps = await getSubtitles({ videoID, lang })
if (Array.isArray(caps) && caps.length > 0) {
// Convert the returned subtitles to our Caption format
const convertedCaps: Caption[] = caps.map((cap: Subtitle) => ({
start: typeof cap.start === 'string' ? parseFloat(cap.start) : cap.start,
dur: typeof cap.dur === 'string' ? parseFloat(cap.dur) : cap.dur,
text: cap.text
}))
return { caps: convertedCaps, lang, isAuto: !manualFirst }
}
} catch {
// try next
}
}
return null
}
export default async function handler(req: VercelRequest, res: VercelResponse) {
const videoId = (req.query.videoId as string | undefined)?.trim()
if (!videoId) return bad(res, 400, 'Missing videoId')
const lang = ((req.query.lang as string | undefined) || 'en').toLowerCase()
const uiLocale = (req.headers['x-ui-locale'] as string | undefined)?.toLowerCase()
const preferAuto = req.query.preferAuto === 'true'
const cacheKey = buildKey(videoId, lang, preferAuto ? 'auto' : undefined)
const now = Date.now()
const cached = memoryCache.get(cacheKey)
if (cached && cached.expires > now) {
return ok(res, cached.body)
}
try {
// Since getVideoDetails doesn't exist, we'll use a simple approach
// In a real implementation, you might want to use YouTube's API or other methods
const title = '' // Will be populated from captions or other sources
const description = ''
// Language order: manual en -> uiLocale -> lang -> any manual, then auto with same order
const langs: string[] = Array.from(new Set(['en', uiLocale, lang].filter(Boolean) as string[]))
let selected = null as null | { caps: Caption[]; lang: string; isAuto: boolean }
// Manual first
selected = await pickCaptions(videoId, langs, true)
if (!selected) {
// Try auto
selected = await pickCaptions(videoId, langs, false)
}
const captions = selected?.caps || []
const transcript = captions.map(c => c.text).join(' ').trim()
const response = {
title,
description,
captions,
transcript,
lang: selected?.lang || lang,
isAuto: selected?.isAuto || false,
source: 'youtube'
}
memoryCache.set(cacheKey, { body: response, expires: now + WEEK_MS })
return ok(res, response)
} catch (e) {
return bad(res, 500, 'Failed to fetch YouTube metadata')
}
}

View File

@@ -1 +0,0 @@
../applesauce

15
dist/index.html vendored
View File

@@ -1,15 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Boris - Nostr Bookmarks</title>
<script type="module" crossorigin src="/assets/index--wClm1wz.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-Bj-Uhit8.css">
</head>
<body>
<div id="root"></div>
</body>
</html>

View File

@@ -2,9 +2,36 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<meta name="theme-color" content="#0f172a" />
<link rel="manifest" href="/manifest.webmanifest" />
<title>Boris - Nostr Bookmarks</title>
<meta name="description" content="Your reading list for the Nostr world. A minimal nostr client for bookmark management with highlights." />
<link rel="canonical" href="https://read.withboris.com/" />
<!-- Open Graph / Social Media -->
<meta property="og:type" content="website" />
<meta property="og:url" content="https://read.withboris.com/" />
<meta property="og:title" content="Boris - Nostr Bookmarks" />
<meta property="og:description" content="Your reading list for the Nostr world. A minimal nostr client for bookmark management with highlights." />
<meta property="og:image" content="https://read.withboris.com/boris-social-1200.png" />
<meta property="og:site_name" content="Boris" />
<!-- Twitter Card -->
<meta name="twitter:card" content="summary_large_image" />
<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." />
<meta name="twitter:image" content="https://read.withboris.com/boris-social-1200.png" />
<!-- Default to system theme until settings load from Nostr -->
<script>
document.documentElement.className = 'theme-system';
</script>
</head>
<body>
<div id="root"></div>

1
node_modules/.bin/acorn generated vendored
View File

@@ -1 +0,0 @@
../acorn/bin/acorn

View File

@@ -1 +0,0 @@
../baseline-browser-mapping/dist/cli.js

1
node_modules/.bin/browserslist generated vendored
View File

@@ -1 +0,0 @@
../browserslist/cli.js

1
node_modules/.bin/esbuild generated vendored
View File

@@ -1 +0,0 @@
../esbuild/bin/esbuild

1
node_modules/.bin/eslint generated vendored
View File

@@ -1 +0,0 @@
../eslint/bin/eslint.js

1
node_modules/.bin/js-yaml generated vendored
View File

@@ -1 +0,0 @@
../js-yaml/bin/js-yaml.js

1
node_modules/.bin/jsesc generated vendored
View File

@@ -1 +0,0 @@
../jsesc/bin/jsesc

1
node_modules/.bin/json5 generated vendored
View File

@@ -1 +0,0 @@
../json5/lib/cli.js

1
node_modules/.bin/loose-envify generated vendored
View File

@@ -1 +0,0 @@
../loose-envify/cli.js

1
node_modules/.bin/nanoid generated vendored
View File

@@ -1 +0,0 @@
../nanoid/bin/nanoid.js

1
node_modules/.bin/node-which generated vendored
View File

@@ -1 +0,0 @@
../which/bin/node-which

1
node_modules/.bin/parser generated vendored
View File

@@ -1 +0,0 @@
../@babel/parser/bin/babel-parser.js

1
node_modules/.bin/rimraf generated vendored
View File

@@ -1 +0,0 @@
../rimraf/bin.js

1
node_modules/.bin/rollup generated vendored
View File

@@ -1 +0,0 @@
../rollup/dist/bin/rollup

1
node_modules/.bin/semver generated vendored
View File

@@ -1 +0,0 @@
../semver/bin/semver.js

1
node_modules/.bin/tsc generated vendored
View File

@@ -1 +0,0 @@
../typescript/bin/tsc

1
node_modules/.bin/tsserver generated vendored
View File

@@ -1 +0,0 @@
../typescript/bin/tsserver

View File

@@ -1 +0,0 @@
../update-browserslist-db/cli.js

1
node_modules/.bin/vite generated vendored
View File

@@ -1 +0,0 @@
../vite/bin/vite.js

5628
node_modules/.package-lock.json generated vendored

File diff suppressed because it is too large Load Diff

View File

@@ -1,22 +0,0 @@
MIT License
Copyright (c) 2014-present Sebastian McKenzie and other contributors
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@@ -1,19 +0,0 @@
# @babel/code-frame
> Generate errors that contain a code frame that point to source locations.
See our website [@babel/code-frame](https://babeljs.io/docs/babel-code-frame) for more information.
## Install
Using npm:
```sh
npm install --save-dev @babel/code-frame
```
or using yarn:
```sh
yarn add @babel/code-frame --dev
```

View File

@@ -1,216 +0,0 @@
'use strict';
Object.defineProperty(exports, '__esModule', { value: true });
var picocolors = require('picocolors');
var jsTokens = require('js-tokens');
var helperValidatorIdentifier = require('@babel/helper-validator-identifier');
function isColorSupported() {
return (typeof process === "object" && (process.env.FORCE_COLOR === "0" || process.env.FORCE_COLOR === "false") ? false : picocolors.isColorSupported
);
}
const compose = (f, g) => v => f(g(v));
function buildDefs(colors) {
return {
keyword: colors.cyan,
capitalized: colors.yellow,
jsxIdentifier: colors.yellow,
punctuator: colors.yellow,
number: colors.magenta,
string: colors.green,
regex: colors.magenta,
comment: colors.gray,
invalid: compose(compose(colors.white, colors.bgRed), colors.bold),
gutter: colors.gray,
marker: compose(colors.red, colors.bold),
message: compose(colors.red, colors.bold),
reset: colors.reset
};
}
const defsOn = buildDefs(picocolors.createColors(true));
const defsOff = buildDefs(picocolors.createColors(false));
function getDefs(enabled) {
return enabled ? defsOn : defsOff;
}
const sometimesKeywords = new Set(["as", "async", "from", "get", "of", "set"]);
const NEWLINE$1 = /\r\n|[\n\r\u2028\u2029]/;
const BRACKET = /^[()[\]{}]$/;
let tokenize;
{
const JSX_TAG = /^[a-z][\w-]*$/i;
const getTokenType = function (token, offset, text) {
if (token.type === "name") {
if (helperValidatorIdentifier.isKeyword(token.value) || helperValidatorIdentifier.isStrictReservedWord(token.value, true) || sometimesKeywords.has(token.value)) {
return "keyword";
}
if (JSX_TAG.test(token.value) && (text[offset - 1] === "<" || text.slice(offset - 2, offset) === "</")) {
return "jsxIdentifier";
}
if (token.value[0] !== token.value[0].toLowerCase()) {
return "capitalized";
}
}
if (token.type === "punctuator" && BRACKET.test(token.value)) {
return "bracket";
}
if (token.type === "invalid" && (token.value === "@" || token.value === "#")) {
return "punctuator";
}
return token.type;
};
tokenize = function* (text) {
let match;
while (match = jsTokens.default.exec(text)) {
const token = jsTokens.matchToToken(match);
yield {
type: getTokenType(token, match.index, text),
value: token.value
};
}
};
}
function highlight(text) {
if (text === "") return "";
const defs = getDefs(true);
let highlighted = "";
for (const {
type,
value
} of tokenize(text)) {
if (type in defs) {
highlighted += value.split(NEWLINE$1).map(str => defs[type](str)).join("\n");
} else {
highlighted += value;
}
}
return highlighted;
}
let deprecationWarningShown = false;
const NEWLINE = /\r\n|[\n\r\u2028\u2029]/;
function getMarkerLines(loc, source, opts) {
const startLoc = Object.assign({
column: 0,
line: -1
}, loc.start);
const endLoc = Object.assign({}, startLoc, loc.end);
const {
linesAbove = 2,
linesBelow = 3
} = opts || {};
const startLine = startLoc.line;
const startColumn = startLoc.column;
const endLine = endLoc.line;
const endColumn = endLoc.column;
let start = Math.max(startLine - (linesAbove + 1), 0);
let end = Math.min(source.length, endLine + linesBelow);
if (startLine === -1) {
start = 0;
}
if (endLine === -1) {
end = source.length;
}
const lineDiff = endLine - startLine;
const markerLines = {};
if (lineDiff) {
for (let i = 0; i <= lineDiff; i++) {
const lineNumber = i + startLine;
if (!startColumn) {
markerLines[lineNumber] = true;
} else if (i === 0) {
const sourceLength = source[lineNumber - 1].length;
markerLines[lineNumber] = [startColumn, sourceLength - startColumn + 1];
} else if (i === lineDiff) {
markerLines[lineNumber] = [0, endColumn];
} else {
const sourceLength = source[lineNumber - i].length;
markerLines[lineNumber] = [0, sourceLength];
}
}
} else {
if (startColumn === endColumn) {
if (startColumn) {
markerLines[startLine] = [startColumn, 0];
} else {
markerLines[startLine] = true;
}
} else {
markerLines[startLine] = [startColumn, endColumn - startColumn];
}
}
return {
start,
end,
markerLines
};
}
function codeFrameColumns(rawLines, loc, opts = {}) {
const shouldHighlight = opts.forceColor || isColorSupported() && opts.highlightCode;
const defs = getDefs(shouldHighlight);
const lines = rawLines.split(NEWLINE);
const {
start,
end,
markerLines
} = getMarkerLines(loc, lines, opts);
const hasColumns = loc.start && typeof loc.start.column === "number";
const numberMaxWidth = String(end).length;
const highlightedLines = shouldHighlight ? highlight(rawLines) : rawLines;
let frame = highlightedLines.split(NEWLINE, end).slice(start, end).map((line, index) => {
const number = start + 1 + index;
const paddedNumber = ` ${number}`.slice(-numberMaxWidth);
const gutter = ` ${paddedNumber} |`;
const hasMarker = markerLines[number];
const lastMarkerLine = !markerLines[number + 1];
if (hasMarker) {
let markerLine = "";
if (Array.isArray(hasMarker)) {
const markerSpacing = line.slice(0, Math.max(hasMarker[0] - 1, 0)).replace(/[^\t]/g, " ");
const numberOfMarkers = hasMarker[1] || 1;
markerLine = ["\n ", defs.gutter(gutter.replace(/\d/g, " ")), " ", markerSpacing, defs.marker("^").repeat(numberOfMarkers)].join("");
if (lastMarkerLine && opts.message) {
markerLine += " " + defs.message(opts.message);
}
}
return [defs.marker(">"), defs.gutter(gutter), line.length > 0 ? ` ${line}` : "", markerLine].join("");
} else {
return ` ${defs.gutter(gutter)}${line.length > 0 ? ` ${line}` : ""}`;
}
}).join("\n");
if (opts.message && !hasColumns) {
frame = `${" ".repeat(numberMaxWidth + 1)}${opts.message}\n${frame}`;
}
if (shouldHighlight) {
return defs.reset(frame);
} else {
return frame;
}
}
function index (rawLines, lineNumber, colNumber, opts = {}) {
if (!deprecationWarningShown) {
deprecationWarningShown = true;
const message = "Passing lineNumber and colNumber is deprecated to @babel/code-frame. Please use `codeFrameColumns`.";
if (process.emitWarning) {
process.emitWarning(message, "DeprecationWarning");
} else {
const deprecationError = new Error(message);
deprecationError.name = "DeprecationWarning";
console.warn(new Error(message));
}
}
colNumber = Math.max(colNumber, 0);
const location = {
start: {
column: colNumber,
line: lineNumber
}
};
return codeFrameColumns(rawLines, location, opts);
}
exports.codeFrameColumns = codeFrameColumns;
exports.default = index;
exports.highlight = highlight;
//# sourceMappingURL=index.js.map

File diff suppressed because one or more lines are too long

View File

@@ -1,31 +0,0 @@
{
"name": "@babel/code-frame",
"version": "7.27.1",
"description": "Generate errors that contain a code frame that point to source locations.",
"author": "The Babel Team (https://babel.dev/team)",
"homepage": "https://babel.dev/docs/en/next/babel-code-frame",
"bugs": "https://github.com/babel/babel/issues?utf8=%E2%9C%93&q=is%3Aissue+is%3Aopen",
"license": "MIT",
"publishConfig": {
"access": "public"
},
"repository": {
"type": "git",
"url": "https://github.com/babel/babel.git",
"directory": "packages/babel-code-frame"
},
"main": "./lib/index.js",
"dependencies": {
"@babel/helper-validator-identifier": "^7.27.1",
"js-tokens": "^4.0.0",
"picocolors": "^1.1.1"
},
"devDependencies": {
"import-meta-resolve": "^4.1.0",
"strip-ansi": "^4.0.0"
},
"engines": {
"node": ">=6.9.0"
},
"type": "commonjs"
}

View File

@@ -1,22 +0,0 @@
MIT License
Copyright (c) 2014-present Sebastian McKenzie and other contributors
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@@ -1,19 +0,0 @@
# @babel/compat-data
> The compat-data to determine required Babel plugins
See our website [@babel/compat-data](https://babeljs.io/docs/babel-compat-data) for more information.
## Install
Using npm:
```sh
npm install --save @babel/compat-data
```
or using yarn:
```sh
yarn add @babel/compat-data
```

View File

@@ -1,2 +0,0 @@
// Todo (Babel 8): remove this file as Babel 8 drop support of core-js 2
module.exports = require("./data/corejs2-built-ins.json");

View File

@@ -1,2 +0,0 @@
// Todo (Babel 8): remove this file now that it is included in babel-plugin-polyfill-corejs3
module.exports = require("./data/corejs3-shipped-proposals.json");

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +0,0 @@
[
"esnext.promise.all-settled",
"esnext.string.match-all",
"esnext.global-this"
]

View File

@@ -1,18 +0,0 @@
{
"es6.module": {
"chrome": "61",
"and_chr": "61",
"edge": "16",
"firefox": "60",
"and_ff": "60",
"node": "13.2.0",
"opera": "48",
"op_mob": "45",
"safari": "10.1",
"ios": "10.3",
"samsung": "8.2",
"android": "61",
"electron": "2.0",
"ios_saf": "10.3"
}
}

View File

@@ -1,35 +0,0 @@
{
"transform-async-to-generator": [
"bugfix/transform-async-arrows-in-class"
],
"transform-parameters": [
"bugfix/transform-edge-default-parameters",
"bugfix/transform-safari-id-destructuring-collision-in-function-expression"
],
"transform-function-name": [
"bugfix/transform-edge-function-name"
],
"transform-block-scoping": [
"bugfix/transform-safari-block-shadowing",
"bugfix/transform-safari-for-shadowing"
],
"transform-template-literals": [
"bugfix/transform-tagged-template-caching"
],
"transform-optional-chaining": [
"bugfix/transform-v8-spread-parameters-in-optional-chaining"
],
"proposal-optional-chaining": [
"bugfix/transform-v8-spread-parameters-in-optional-chaining"
],
"transform-class-properties": [
"bugfix/transform-v8-static-class-fields-redefine-readonly",
"bugfix/transform-firefox-class-in-computed-class-key",
"bugfix/transform-safari-class-field-initializer-scope"
],
"proposal-class-properties": [
"bugfix/transform-v8-static-class-fields-redefine-readonly",
"bugfix/transform-firefox-class-in-computed-class-key",
"bugfix/transform-safari-class-field-initializer-scope"
]
}

View File

@@ -1,203 +0,0 @@
{
"bugfix/transform-async-arrows-in-class": {
"chrome": "55",
"opera": "42",
"edge": "15",
"firefox": "52",
"safari": "11",
"node": "7.6",
"deno": "1",
"ios": "11",
"samsung": "6",
"opera_mobile": "42",
"electron": "1.6"
},
"bugfix/transform-edge-default-parameters": {
"chrome": "49",
"opera": "36",
"edge": "18",
"firefox": "52",
"safari": "10",
"node": "6",
"deno": "1",
"ios": "10",
"samsung": "5",
"opera_mobile": "36",
"electron": "0.37"
},
"bugfix/transform-edge-function-name": {
"chrome": "51",
"opera": "38",
"edge": "79",
"firefox": "53",
"safari": "10",
"node": "6.5",
"deno": "1",
"ios": "10",
"samsung": "5",
"opera_mobile": "41",
"electron": "1.2"
},
"bugfix/transform-safari-block-shadowing": {
"chrome": "49",
"opera": "36",
"edge": "12",
"firefox": "44",
"safari": "11",
"node": "6",
"deno": "1",
"ie": "11",
"ios": "11",
"samsung": "5",
"opera_mobile": "36",
"electron": "0.37"
},
"bugfix/transform-safari-for-shadowing": {
"chrome": "49",
"opera": "36",
"edge": "12",
"firefox": "4",
"safari": "11",
"node": "6",
"deno": "1",
"ie": "11",
"ios": "11",
"samsung": "5",
"rhino": "1.7.13",
"opera_mobile": "36",
"electron": "0.37"
},
"bugfix/transform-safari-id-destructuring-collision-in-function-expression": {
"chrome": "49",
"opera": "36",
"edge": "14",
"firefox": "2",
"safari": "16.3",
"node": "6",
"deno": "1",
"ios": "16.3",
"samsung": "5",
"opera_mobile": "36",
"electron": "0.37"
},
"bugfix/transform-tagged-template-caching": {
"chrome": "41",
"opera": "28",
"edge": "12",
"firefox": "34",
"safari": "13",
"node": "4",
"deno": "1",
"ios": "13",
"samsung": "3.4",
"rhino": "1.7.14",
"opera_mobile": "28",
"electron": "0.21"
},
"bugfix/transform-v8-spread-parameters-in-optional-chaining": {
"chrome": "91",
"opera": "77",
"edge": "91",
"firefox": "74",
"safari": "13.1",
"node": "16.9",
"deno": "1.9",
"ios": "13.4",
"samsung": "16",
"opera_mobile": "64",
"electron": "13.0"
},
"transform-optional-chaining": {
"chrome": "80",
"opera": "67",
"edge": "80",
"firefox": "74",
"safari": "13.1",
"node": "14",
"deno": "1",
"ios": "13.4",
"samsung": "13",
"rhino": "1.8",
"opera_mobile": "57",
"electron": "8.0"
},
"proposal-optional-chaining": {
"chrome": "80",
"opera": "67",
"edge": "80",
"firefox": "74",
"safari": "13.1",
"node": "14",
"deno": "1",
"ios": "13.4",
"samsung": "13",
"rhino": "1.8",
"opera_mobile": "57",
"electron": "8.0"
},
"transform-parameters": {
"chrome": "49",
"opera": "36",
"edge": "15",
"firefox": "52",
"safari": "10",
"node": "6",
"deno": "1",
"ios": "10",
"samsung": "5",
"opera_mobile": "36",
"electron": "0.37"
},
"transform-async-to-generator": {
"chrome": "55",
"opera": "42",
"edge": "15",
"firefox": "52",
"safari": "10.1",
"node": "7.6",
"deno": "1",
"ios": "10.3",
"samsung": "6",
"opera_mobile": "42",
"electron": "1.6"
},
"transform-template-literals": {
"chrome": "41",
"opera": "28",
"edge": "13",
"firefox": "34",
"safari": "9",
"node": "4",
"deno": "1",
"ios": "9",
"samsung": "3.4",
"opera_mobile": "28",
"electron": "0.21"
},
"transform-function-name": {
"chrome": "51",
"opera": "38",
"edge": "14",
"firefox": "53",
"safari": "10",
"node": "6.5",
"deno": "1",
"ios": "10",
"samsung": "5",
"opera_mobile": "41",
"electron": "1.2"
},
"transform-block-scoping": {
"chrome": "50",
"opera": "37",
"edge": "14",
"firefox": "53",
"safari": "10",
"node": "6",
"deno": "1",
"ios": "10",
"samsung": "5",
"opera_mobile": "37",
"electron": "1.1"
}
}

View File

@@ -1,838 +0,0 @@
{
"transform-explicit-resource-management": {
"chrome": "134",
"edge": "134",
"firefox": "141",
"node": "24",
"electron": "35.0"
},
"transform-duplicate-named-capturing-groups-regex": {
"chrome": "126",
"opera": "112",
"edge": "126",
"firefox": "129",
"safari": "17.4",
"node": "23",
"ios": "17.4",
"electron": "31.0"
},
"transform-regexp-modifiers": {
"chrome": "125",
"opera": "111",
"edge": "125",
"firefox": "132",
"node": "23",
"samsung": "27",
"electron": "31.0"
},
"transform-unicode-sets-regex": {
"chrome": "112",
"opera": "98",
"edge": "112",
"firefox": "116",
"safari": "17",
"node": "20",
"deno": "1.32",
"ios": "17",
"samsung": "23",
"opera_mobile": "75",
"electron": "24.0"
},
"bugfix/transform-v8-static-class-fields-redefine-readonly": {
"chrome": "98",
"opera": "84",
"edge": "98",
"firefox": "75",
"safari": "15",
"node": "12",
"deno": "1.18",
"ios": "15",
"samsung": "11",
"opera_mobile": "52",
"electron": "17.0"
},
"bugfix/transform-firefox-class-in-computed-class-key": {
"chrome": "74",
"opera": "62",
"edge": "79",
"firefox": "126",
"safari": "16",
"node": "12",
"deno": "1",
"ios": "16",
"samsung": "11",
"opera_mobile": "53",
"electron": "6.0"
},
"bugfix/transform-safari-class-field-initializer-scope": {
"chrome": "74",
"opera": "62",
"edge": "79",
"firefox": "69",
"safari": "16",
"node": "12",
"deno": "1",
"ios": "16",
"samsung": "11",
"opera_mobile": "53",
"electron": "6.0"
},
"transform-class-static-block": {
"chrome": "94",
"opera": "80",
"edge": "94",
"firefox": "93",
"safari": "16.4",
"node": "16.11",
"deno": "1.14",
"ios": "16.4",
"samsung": "17",
"opera_mobile": "66",
"electron": "15.0"
},
"proposal-class-static-block": {
"chrome": "94",
"opera": "80",
"edge": "94",
"firefox": "93",
"safari": "16.4",
"node": "16.11",
"deno": "1.14",
"ios": "16.4",
"samsung": "17",
"opera_mobile": "66",
"electron": "15.0"
},
"transform-private-property-in-object": {
"chrome": "91",
"opera": "77",
"edge": "91",
"firefox": "90",
"safari": "15",
"node": "16.9",
"deno": "1.9",
"ios": "15",
"samsung": "16",
"opera_mobile": "64",
"electron": "13.0"
},
"proposal-private-property-in-object": {
"chrome": "91",
"opera": "77",
"edge": "91",
"firefox": "90",
"safari": "15",
"node": "16.9",
"deno": "1.9",
"ios": "15",
"samsung": "16",
"opera_mobile": "64",
"electron": "13.0"
},
"transform-class-properties": {
"chrome": "74",
"opera": "62",
"edge": "79",
"firefox": "90",
"safari": "14.1",
"node": "12",
"deno": "1",
"ios": "14.5",
"samsung": "11",
"opera_mobile": "53",
"electron": "6.0"
},
"proposal-class-properties": {
"chrome": "74",
"opera": "62",
"edge": "79",
"firefox": "90",
"safari": "14.1",
"node": "12",
"deno": "1",
"ios": "14.5",
"samsung": "11",
"opera_mobile": "53",
"electron": "6.0"
},
"transform-private-methods": {
"chrome": "84",
"opera": "70",
"edge": "84",
"firefox": "90",
"safari": "15",
"node": "14.6",
"deno": "1",
"ios": "15",
"samsung": "14",
"opera_mobile": "60",
"electron": "10.0"
},
"proposal-private-methods": {
"chrome": "84",
"opera": "70",
"edge": "84",
"firefox": "90",
"safari": "15",
"node": "14.6",
"deno": "1",
"ios": "15",
"samsung": "14",
"opera_mobile": "60",
"electron": "10.0"
},
"transform-numeric-separator": {
"chrome": "75",
"opera": "62",
"edge": "79",
"firefox": "70",
"safari": "13",
"node": "12.5",
"deno": "1",
"ios": "13",
"samsung": "11",
"rhino": "1.7.14",
"opera_mobile": "54",
"electron": "6.0"
},
"proposal-numeric-separator": {
"chrome": "75",
"opera": "62",
"edge": "79",
"firefox": "70",
"safari": "13",
"node": "12.5",
"deno": "1",
"ios": "13",
"samsung": "11",
"rhino": "1.7.14",
"opera_mobile": "54",
"electron": "6.0"
},
"transform-logical-assignment-operators": {
"chrome": "85",
"opera": "71",
"edge": "85",
"firefox": "79",
"safari": "14",
"node": "15",
"deno": "1.2",
"ios": "14",
"samsung": "14",
"opera_mobile": "60",
"electron": "10.0"
},
"proposal-logical-assignment-operators": {
"chrome": "85",
"opera": "71",
"edge": "85",
"firefox": "79",
"safari": "14",
"node": "15",
"deno": "1.2",
"ios": "14",
"samsung": "14",
"opera_mobile": "60",
"electron": "10.0"
},
"transform-nullish-coalescing-operator": {
"chrome": "80",
"opera": "67",
"edge": "80",
"firefox": "72",
"safari": "13.1",
"node": "14",
"deno": "1",
"ios": "13.4",
"samsung": "13",
"rhino": "1.8",
"opera_mobile": "57",
"electron": "8.0"
},
"proposal-nullish-coalescing-operator": {
"chrome": "80",
"opera": "67",
"edge": "80",
"firefox": "72",
"safari": "13.1",
"node": "14",
"deno": "1",
"ios": "13.4",
"samsung": "13",
"rhino": "1.8",
"opera_mobile": "57",
"electron": "8.0"
},
"transform-optional-chaining": {
"chrome": "91",
"opera": "77",
"edge": "91",
"firefox": "74",
"safari": "13.1",
"node": "16.9",
"deno": "1.9",
"ios": "13.4",
"samsung": "16",
"opera_mobile": "64",
"electron": "13.0"
},
"proposal-optional-chaining": {
"chrome": "91",
"opera": "77",
"edge": "91",
"firefox": "74",
"safari": "13.1",
"node": "16.9",
"deno": "1.9",
"ios": "13.4",
"samsung": "16",
"opera_mobile": "64",
"electron": "13.0"
},
"transform-json-strings": {
"chrome": "66",
"opera": "53",
"edge": "79",
"firefox": "62",
"safari": "12",
"node": "10",
"deno": "1",
"ios": "12",
"samsung": "9",
"rhino": "1.7.14",
"opera_mobile": "47",
"electron": "3.0"
},
"proposal-json-strings": {
"chrome": "66",
"opera": "53",
"edge": "79",
"firefox": "62",
"safari": "12",
"node": "10",
"deno": "1",
"ios": "12",
"samsung": "9",
"rhino": "1.7.14",
"opera_mobile": "47",
"electron": "3.0"
},
"transform-optional-catch-binding": {
"chrome": "66",
"opera": "53",
"edge": "79",
"firefox": "58",
"safari": "11.1",
"node": "10",
"deno": "1",
"ios": "11.3",
"samsung": "9",
"opera_mobile": "47",
"electron": "3.0"
},
"proposal-optional-catch-binding": {
"chrome": "66",
"opera": "53",
"edge": "79",
"firefox": "58",
"safari": "11.1",
"node": "10",
"deno": "1",
"ios": "11.3",
"samsung": "9",
"opera_mobile": "47",
"electron": "3.0"
},
"transform-parameters": {
"chrome": "49",
"opera": "36",
"edge": "18",
"firefox": "52",
"safari": "16.3",
"node": "6",
"deno": "1",
"ios": "16.3",
"samsung": "5",
"opera_mobile": "36",
"electron": "0.37"
},
"transform-async-generator-functions": {
"chrome": "63",
"opera": "50",
"edge": "79",
"firefox": "57",
"safari": "12",
"node": "10",
"deno": "1",
"ios": "12",
"samsung": "8",
"opera_mobile": "46",
"electron": "3.0"
},
"proposal-async-generator-functions": {
"chrome": "63",
"opera": "50",
"edge": "79",
"firefox": "57",
"safari": "12",
"node": "10",
"deno": "1",
"ios": "12",
"samsung": "8",
"opera_mobile": "46",
"electron": "3.0"
},
"transform-object-rest-spread": {
"chrome": "60",
"opera": "47",
"edge": "79",
"firefox": "55",
"safari": "11.1",
"node": "8.3",
"deno": "1",
"ios": "11.3",
"samsung": "8",
"opera_mobile": "44",
"electron": "2.0"
},
"proposal-object-rest-spread": {
"chrome": "60",
"opera": "47",
"edge": "79",
"firefox": "55",
"safari": "11.1",
"node": "8.3",
"deno": "1",
"ios": "11.3",
"samsung": "8",
"opera_mobile": "44",
"electron": "2.0"
},
"transform-dotall-regex": {
"chrome": "62",
"opera": "49",
"edge": "79",
"firefox": "78",
"safari": "11.1",
"node": "8.10",
"deno": "1",
"ios": "11.3",
"samsung": "8",
"rhino": "1.7.15",
"opera_mobile": "46",
"electron": "3.0"
},
"transform-unicode-property-regex": {
"chrome": "64",
"opera": "51",
"edge": "79",
"firefox": "78",
"safari": "11.1",
"node": "10",
"deno": "1",
"ios": "11.3",
"samsung": "9",
"opera_mobile": "47",
"electron": "3.0"
},
"proposal-unicode-property-regex": {
"chrome": "64",
"opera": "51",
"edge": "79",
"firefox": "78",
"safari": "11.1",
"node": "10",
"deno": "1",
"ios": "11.3",
"samsung": "9",
"opera_mobile": "47",
"electron": "3.0"
},
"transform-named-capturing-groups-regex": {
"chrome": "64",
"opera": "51",
"edge": "79",
"firefox": "78",
"safari": "11.1",
"node": "10",
"deno": "1",
"ios": "11.3",
"samsung": "9",
"opera_mobile": "47",
"electron": "3.0"
},
"transform-async-to-generator": {
"chrome": "55",
"opera": "42",
"edge": "15",
"firefox": "52",
"safari": "11",
"node": "7.6",
"deno": "1",
"ios": "11",
"samsung": "6",
"opera_mobile": "42",
"electron": "1.6"
},
"transform-exponentiation-operator": {
"chrome": "52",
"opera": "39",
"edge": "14",
"firefox": "52",
"safari": "10.1",
"node": "7",
"deno": "1",
"ios": "10.3",
"samsung": "6",
"rhino": "1.7.14",
"opera_mobile": "41",
"electron": "1.3"
},
"transform-template-literals": {
"chrome": "41",
"opera": "28",
"edge": "13",
"firefox": "34",
"safari": "13",
"node": "4",
"deno": "1",
"ios": "13",
"samsung": "3.4",
"opera_mobile": "28",
"electron": "0.21"
},
"transform-literals": {
"chrome": "44",
"opera": "31",
"edge": "12",
"firefox": "53",
"safari": "9",
"node": "4",
"deno": "1",
"ios": "9",
"samsung": "4",
"rhino": "1.7.15",
"opera_mobile": "32",
"electron": "0.30"
},
"transform-function-name": {
"chrome": "51",
"opera": "38",
"edge": "79",
"firefox": "53",
"safari": "10",
"node": "6.5",
"deno": "1",
"ios": "10",
"samsung": "5",
"opera_mobile": "41",
"electron": "1.2"
},
"transform-arrow-functions": {
"chrome": "47",
"opera": "34",
"edge": "13",
"firefox": "43",
"safari": "10",
"node": "6",
"deno": "1",
"ios": "10",
"samsung": "5",
"rhino": "1.7.13",
"opera_mobile": "34",
"electron": "0.36"
},
"transform-block-scoped-functions": {
"chrome": "41",
"opera": "28",
"edge": "12",
"firefox": "46",
"safari": "10",
"node": "4",
"deno": "1",
"ie": "11",
"ios": "10",
"samsung": "3.4",
"opera_mobile": "28",
"electron": "0.21"
},
"transform-classes": {
"chrome": "46",
"opera": "33",
"edge": "13",
"firefox": "45",
"safari": "10",
"node": "5",
"deno": "1",
"ios": "10",
"samsung": "5",
"opera_mobile": "33",
"electron": "0.36"
},
"transform-object-super": {
"chrome": "46",
"opera": "33",
"edge": "13",
"firefox": "45",
"safari": "10",
"node": "5",
"deno": "1",
"ios": "10",
"samsung": "5",
"opera_mobile": "33",
"electron": "0.36"
},
"transform-shorthand-properties": {
"chrome": "43",
"opera": "30",
"edge": "12",
"firefox": "33",
"safari": "9",
"node": "4",
"deno": "1",
"ios": "9",
"samsung": "4",
"rhino": "1.7.14",
"opera_mobile": "30",
"electron": "0.27"
},
"transform-duplicate-keys": {
"chrome": "42",
"opera": "29",
"edge": "12",
"firefox": "34",
"safari": "9",
"node": "4",
"deno": "1",
"ios": "9",
"samsung": "3.4",
"opera_mobile": "29",
"electron": "0.25"
},
"transform-computed-properties": {
"chrome": "44",
"opera": "31",
"edge": "12",
"firefox": "34",
"safari": "7.1",
"node": "4",
"deno": "1",
"ios": "8",
"samsung": "4",
"rhino": "1.8",
"opera_mobile": "32",
"electron": "0.30"
},
"transform-for-of": {
"chrome": "51",
"opera": "38",
"edge": "15",
"firefox": "53",
"safari": "10",
"node": "6.5",
"deno": "1",
"ios": "10",
"samsung": "5",
"opera_mobile": "41",
"electron": "1.2"
},
"transform-sticky-regex": {
"chrome": "49",
"opera": "36",
"edge": "13",
"firefox": "3",
"safari": "10",
"node": "6",
"deno": "1",
"ios": "10",
"samsung": "5",
"rhino": "1.7.15",
"opera_mobile": "36",
"electron": "0.37"
},
"transform-unicode-escapes": {
"chrome": "44",
"opera": "31",
"edge": "12",
"firefox": "53",
"safari": "9",
"node": "4",
"deno": "1",
"ios": "9",
"samsung": "4",
"rhino": "1.7.15",
"opera_mobile": "32",
"electron": "0.30"
},
"transform-unicode-regex": {
"chrome": "50",
"opera": "37",
"edge": "13",
"firefox": "46",
"safari": "12",
"node": "6",
"deno": "1",
"ios": "12",
"samsung": "5",
"opera_mobile": "37",
"electron": "1.1"
},
"transform-spread": {
"chrome": "46",
"opera": "33",
"edge": "13",
"firefox": "45",
"safari": "10",
"node": "5",
"deno": "1",
"ios": "10",
"samsung": "5",
"opera_mobile": "33",
"electron": "0.36"
},
"transform-destructuring": {
"chrome": "51",
"opera": "38",
"edge": "15",
"firefox": "53",
"safari": "10",
"node": "6.5",
"deno": "1",
"ios": "10",
"samsung": "5",
"opera_mobile": "41",
"electron": "1.2"
},
"transform-block-scoping": {
"chrome": "50",
"opera": "37",
"edge": "14",
"firefox": "53",
"safari": "11",
"node": "6",
"deno": "1",
"ios": "11",
"samsung": "5",
"opera_mobile": "37",
"electron": "1.1"
},
"transform-typeof-symbol": {
"chrome": "48",
"opera": "35",
"edge": "12",
"firefox": "36",
"safari": "9",
"node": "6",
"deno": "1",
"ios": "9",
"samsung": "5",
"rhino": "1.8",
"opera_mobile": "35",
"electron": "0.37"
},
"transform-new-target": {
"chrome": "46",
"opera": "33",
"edge": "14",
"firefox": "41",
"safari": "10",
"node": "5",
"deno": "1",
"ios": "10",
"samsung": "5",
"opera_mobile": "33",
"electron": "0.36"
},
"transform-regenerator": {
"chrome": "50",
"opera": "37",
"edge": "13",
"firefox": "53",
"safari": "10",
"node": "6",
"deno": "1",
"ios": "10",
"samsung": "5",
"opera_mobile": "37",
"electron": "1.1"
},
"transform-member-expression-literals": {
"chrome": "7",
"opera": "12",
"edge": "12",
"firefox": "2",
"safari": "5.1",
"node": "0.4",
"deno": "1",
"ie": "9",
"android": "4",
"ios": "6",
"phantom": "1.9",
"samsung": "1",
"rhino": "1.7.13",
"opera_mobile": "12",
"electron": "0.20"
},
"transform-property-literals": {
"chrome": "7",
"opera": "12",
"edge": "12",
"firefox": "2",
"safari": "5.1",
"node": "0.4",
"deno": "1",
"ie": "9",
"android": "4",
"ios": "6",
"phantom": "1.9",
"samsung": "1",
"rhino": "1.7.13",
"opera_mobile": "12",
"electron": "0.20"
},
"transform-reserved-words": {
"chrome": "13",
"opera": "10.50",
"edge": "12",
"firefox": "2",
"safari": "3.1",
"node": "0.6",
"deno": "1",
"ie": "9",
"android": "4.4",
"ios": "6",
"phantom": "1.9",
"samsung": "1",
"rhino": "1.7.13",
"opera_mobile": "10.1",
"electron": "0.20"
},
"transform-export-namespace-from": {
"chrome": "72",
"deno": "1.0",
"edge": "79",
"firefox": "80",
"node": "13.2.0",
"opera": "60",
"opera_mobile": "51",
"safari": "14.1",
"ios": "14.5",
"samsung": "11.0",
"android": "72",
"electron": "5.0"
},
"proposal-export-namespace-from": {
"chrome": "72",
"deno": "1.0",
"edge": "79",
"firefox": "80",
"node": "13.2.0",
"opera": "60",
"opera_mobile": "51",
"safari": "14.1",
"ios": "14.5",
"samsung": "11.0",
"android": "72",
"electron": "5.0"
}
}

View File

@@ -1,2 +0,0 @@
// Todo (Babel 8): remove this file, in Babel 8 users import the .json directly
module.exports = require("./data/native-modules.json");

View File

@@ -1,2 +0,0 @@
// Todo (Babel 8): remove this file, in Babel 8 users import the .json directly
module.exports = require("./data/overlapping-plugins.json");

View File

@@ -1,40 +0,0 @@
{
"name": "@babel/compat-data",
"version": "7.28.4",
"author": "The Babel Team (https://babel.dev/team)",
"license": "MIT",
"description": "The compat-data to determine required Babel plugins",
"repository": {
"type": "git",
"url": "https://github.com/babel/babel.git",
"directory": "packages/babel-compat-data"
},
"publishConfig": {
"access": "public"
},
"exports": {
"./plugins": "./plugins.js",
"./native-modules": "./native-modules.js",
"./corejs2-built-ins": "./corejs2-built-ins.js",
"./corejs3-shipped-proposals": "./corejs3-shipped-proposals.js",
"./overlapping-plugins": "./overlapping-plugins.js",
"./plugin-bugfixes": "./plugin-bugfixes.js"
},
"scripts": {
"build-data": "./scripts/download-compat-table.sh && node ./scripts/build-data.mjs && node ./scripts/build-modules-support.mjs && node ./scripts/build-bugfixes-targets.mjs"
},
"keywords": [
"babel",
"compat-table",
"compat-data"
],
"devDependencies": {
"@mdn/browser-compat-data": "^6.0.8",
"core-js-compat": "^3.43.0",
"electron-to-chromium": "^1.5.140"
},
"engines": {
"node": ">=6.9.0"
},
"type": "commonjs"
}

View File

@@ -1,2 +0,0 @@
// Todo (Babel 8): remove this file, in Babel 8 users import the .json directly
module.exports = require("./data/plugin-bugfixes.json");

View File

@@ -1,2 +0,0 @@
// Todo (Babel 8): remove this file, in Babel 8 users import the .json directly
module.exports = require("./data/plugins.json");

22
node_modules/@babel/core/LICENSE generated vendored
View File

@@ -1,22 +0,0 @@
MIT License
Copyright (c) 2014-present Sebastian McKenzie and other contributors
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

19
node_modules/@babel/core/README.md generated vendored
View File

@@ -1,19 +0,0 @@
# @babel/core
> Babel compiler core.
See our website [@babel/core](https://babeljs.io/docs/babel-core) for more information or the [issues](https://github.com/babel/babel/issues?utf8=%E2%9C%93&q=is%3Aissue+label%3A%22pkg%3A%20core%22+is%3Aopen) associated with this package.
## Install
Using npm:
```sh
npm install --save-dev @babel/core
```
or using yarn:
```sh
yarn add @babel/core --dev
```

View File

@@ -1,5 +0,0 @@
"use strict";
0 && 0;
//# sourceMappingURL=cache-contexts.js.map

View File

@@ -1 +0,0 @@
{"version":3,"names":[],"sources":["../../src/config/cache-contexts.ts"],"sourcesContent":["import type { Targets } from \"@babel/helper-compilation-targets\";\n\nimport type { ConfigContext } from \"./config-chain.ts\";\nimport type { CallerMetadata } from \"./validation/options.ts\";\n\nexport type { ConfigContext as FullConfig };\n\nexport type FullPreset = {\n targets: Targets;\n} & ConfigContext;\nexport type FullPlugin = {\n assumptions: { [name: string]: boolean };\n} & FullPreset;\n\n// Context not including filename since it is used in places that cannot\n// process 'ignore'/'only' and other filename-based logic.\nexport type SimpleConfig = {\n envName: string;\n caller: CallerMetadata | undefined;\n};\nexport type SimplePreset = {\n targets: Targets;\n} & SimpleConfig;\nexport type SimplePlugin = {\n assumptions: {\n [name: string]: boolean;\n };\n} & SimplePreset;\n"],"mappings":"","ignoreList":[]}

View File

@@ -1,261 +0,0 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.assertSimpleType = assertSimpleType;
exports.makeStrongCache = makeStrongCache;
exports.makeStrongCacheSync = makeStrongCacheSync;
exports.makeWeakCache = makeWeakCache;
exports.makeWeakCacheSync = makeWeakCacheSync;
function _gensync() {
const data = require("gensync");
_gensync = function () {
return data;
};
return data;
}
var _async = require("../gensync-utils/async.js");
var _util = require("./util.js");
const synchronize = gen => {
return _gensync()(gen).sync;
};
function* genTrue() {
return true;
}
function makeWeakCache(handler) {
return makeCachedFunction(WeakMap, handler);
}
function makeWeakCacheSync(handler) {
return synchronize(makeWeakCache(handler));
}
function makeStrongCache(handler) {
return makeCachedFunction(Map, handler);
}
function makeStrongCacheSync(handler) {
return synchronize(makeStrongCache(handler));
}
function makeCachedFunction(CallCache, handler) {
const callCacheSync = new CallCache();
const callCacheAsync = new CallCache();
const futureCache = new CallCache();
return function* cachedFunction(arg, data) {
const asyncContext = yield* (0, _async.isAsync)();
const callCache = asyncContext ? callCacheAsync : callCacheSync;
const cached = yield* getCachedValueOrWait(asyncContext, callCache, futureCache, arg, data);
if (cached.valid) return cached.value;
const cache = new CacheConfigurator(data);
const handlerResult = handler(arg, cache);
let finishLock;
let value;
if ((0, _util.isIterableIterator)(handlerResult)) {
value = yield* (0, _async.onFirstPause)(handlerResult, () => {
finishLock = setupAsyncLocks(cache, futureCache, arg);
});
} else {
value = handlerResult;
}
updateFunctionCache(callCache, cache, arg, value);
if (finishLock) {
futureCache.delete(arg);
finishLock.release(value);
}
return value;
};
}
function* getCachedValue(cache, arg, data) {
const cachedValue = cache.get(arg);
if (cachedValue) {
for (const {
value,
valid
} of cachedValue) {
if (yield* valid(data)) return {
valid: true,
value
};
}
}
return {
valid: false,
value: null
};
}
function* getCachedValueOrWait(asyncContext, callCache, futureCache, arg, data) {
const cached = yield* getCachedValue(callCache, arg, data);
if (cached.valid) {
return cached;
}
if (asyncContext) {
const cached = yield* getCachedValue(futureCache, arg, data);
if (cached.valid) {
const value = yield* (0, _async.waitFor)(cached.value.promise);
return {
valid: true,
value
};
}
}
return {
valid: false,
value: null
};
}
function setupAsyncLocks(config, futureCache, arg) {
const finishLock = new Lock();
updateFunctionCache(futureCache, config, arg, finishLock);
return finishLock;
}
function updateFunctionCache(cache, config, arg, value) {
if (!config.configured()) config.forever();
let cachedValue = cache.get(arg);
config.deactivate();
switch (config.mode()) {
case "forever":
cachedValue = [{
value,
valid: genTrue
}];
cache.set(arg, cachedValue);
break;
case "invalidate":
cachedValue = [{
value,
valid: config.validator()
}];
cache.set(arg, cachedValue);
break;
case "valid":
if (cachedValue) {
cachedValue.push({
value,
valid: config.validator()
});
} else {
cachedValue = [{
value,
valid: config.validator()
}];
cache.set(arg, cachedValue);
}
}
}
class CacheConfigurator {
constructor(data) {
this._active = true;
this._never = false;
this._forever = false;
this._invalidate = false;
this._configured = false;
this._pairs = [];
this._data = void 0;
this._data = data;
}
simple() {
return makeSimpleConfigurator(this);
}
mode() {
if (this._never) return "never";
if (this._forever) return "forever";
if (this._invalidate) return "invalidate";
return "valid";
}
forever() {
if (!this._active) {
throw new Error("Cannot change caching after evaluation has completed.");
}
if (this._never) {
throw new Error("Caching has already been configured with .never()");
}
this._forever = true;
this._configured = true;
}
never() {
if (!this._active) {
throw new Error("Cannot change caching after evaluation has completed.");
}
if (this._forever) {
throw new Error("Caching has already been configured with .forever()");
}
this._never = true;
this._configured = true;
}
using(handler) {
if (!this._active) {
throw new Error("Cannot change caching after evaluation has completed.");
}
if (this._never || this._forever) {
throw new Error("Caching has already been configured with .never or .forever()");
}
this._configured = true;
const key = handler(this._data);
const fn = (0, _async.maybeAsync)(handler, `You appear to be using an async cache handler, but Babel has been called synchronously`);
if ((0, _async.isThenable)(key)) {
return key.then(key => {
this._pairs.push([key, fn]);
return key;
});
}
this._pairs.push([key, fn]);
return key;
}
invalidate(handler) {
this._invalidate = true;
return this.using(handler);
}
validator() {
const pairs = this._pairs;
return function* (data) {
for (const [key, fn] of pairs) {
if (key !== (yield* fn(data))) return false;
}
return true;
};
}
deactivate() {
this._active = false;
}
configured() {
return this._configured;
}
}
function makeSimpleConfigurator(cache) {
function cacheFn(val) {
if (typeof val === "boolean") {
if (val) cache.forever();else cache.never();
return;
}
return cache.using(() => assertSimpleType(val()));
}
cacheFn.forever = () => cache.forever();
cacheFn.never = () => cache.never();
cacheFn.using = cb => cache.using(() => assertSimpleType(cb()));
cacheFn.invalidate = cb => cache.invalidate(() => assertSimpleType(cb()));
return cacheFn;
}
function assertSimpleType(value) {
if ((0, _async.isThenable)(value)) {
throw new Error(`You appear to be using an async cache handler, ` + `which your current version of Babel does not support. ` + `We may add support for this in the future, ` + `but if you're on the most recent version of @babel/core and still ` + `seeing this error, then you'll need to synchronously handle your caching logic.`);
}
if (value != null && typeof value !== "string" && typeof value !== "boolean" && typeof value !== "number") {
throw new Error("Cache keys must be either string, boolean, number, null, or undefined.");
}
return value;
}
class Lock {
constructor() {
this.released = false;
this.promise = void 0;
this._resolve = void 0;
this.promise = new Promise(resolve => {
this._resolve = resolve;
});
}
release(value) {
this.released = true;
this._resolve(value);
}
}
0 && 0;
//# sourceMappingURL=caching.js.map

File diff suppressed because one or more lines are too long

View File

@@ -1,469 +0,0 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.buildPresetChain = buildPresetChain;
exports.buildPresetChainWalker = void 0;
exports.buildRootChain = buildRootChain;
function _path() {
const data = require("path");
_path = function () {
return data;
};
return data;
}
function _debug() {
const data = require("debug");
_debug = function () {
return data;
};
return data;
}
var _options = require("./validation/options.js");
var _patternToRegex = require("./pattern-to-regex.js");
var _printer = require("./printer.js");
var _rewriteStackTrace = require("../errors/rewrite-stack-trace.js");
var _configError = require("../errors/config-error.js");
var _index = require("./files/index.js");
var _caching = require("./caching.js");
var _configDescriptors = require("./config-descriptors.js");
const debug = _debug()("babel:config:config-chain");
function* buildPresetChain(arg, context) {
const chain = yield* buildPresetChainWalker(arg, context);
if (!chain) return null;
return {
plugins: dedupDescriptors(chain.plugins),
presets: dedupDescriptors(chain.presets),
options: chain.options.map(o => normalizeOptions(o)),
files: new Set()
};
}
const buildPresetChainWalker = exports.buildPresetChainWalker = makeChainWalker({
root: preset => loadPresetDescriptors(preset),
env: (preset, envName) => loadPresetEnvDescriptors(preset)(envName),
overrides: (preset, index) => loadPresetOverridesDescriptors(preset)(index),
overridesEnv: (preset, index, envName) => loadPresetOverridesEnvDescriptors(preset)(index)(envName),
createLogger: () => () => {}
});
const loadPresetDescriptors = (0, _caching.makeWeakCacheSync)(preset => buildRootDescriptors(preset, preset.alias, _configDescriptors.createUncachedDescriptors));
const loadPresetEnvDescriptors = (0, _caching.makeWeakCacheSync)(preset => (0, _caching.makeStrongCacheSync)(envName => buildEnvDescriptors(preset, preset.alias, _configDescriptors.createUncachedDescriptors, envName)));
const loadPresetOverridesDescriptors = (0, _caching.makeWeakCacheSync)(preset => (0, _caching.makeStrongCacheSync)(index => buildOverrideDescriptors(preset, preset.alias, _configDescriptors.createUncachedDescriptors, index)));
const loadPresetOverridesEnvDescriptors = (0, _caching.makeWeakCacheSync)(preset => (0, _caching.makeStrongCacheSync)(index => (0, _caching.makeStrongCacheSync)(envName => buildOverrideEnvDescriptors(preset, preset.alias, _configDescriptors.createUncachedDescriptors, index, envName))));
function* buildRootChain(opts, context) {
let configReport, babelRcReport;
const programmaticLogger = new _printer.ConfigPrinter();
const programmaticChain = yield* loadProgrammaticChain({
options: opts,
dirname: context.cwd
}, context, undefined, programmaticLogger);
if (!programmaticChain) return null;
const programmaticReport = yield* programmaticLogger.output();
let configFile;
if (typeof opts.configFile === "string") {
configFile = yield* (0, _index.loadConfig)(opts.configFile, context.cwd, context.envName, context.caller);
} else if (opts.configFile !== false) {
configFile = yield* (0, _index.findRootConfig)(context.root, context.envName, context.caller);
}
let {
babelrc,
babelrcRoots
} = opts;
let babelrcRootsDirectory = context.cwd;
const configFileChain = emptyChain();
const configFileLogger = new _printer.ConfigPrinter();
if (configFile) {
const validatedFile = validateConfigFile(configFile);
const result = yield* loadFileChain(validatedFile, context, undefined, configFileLogger);
if (!result) return null;
configReport = yield* configFileLogger.output();
if (babelrc === undefined) {
babelrc = validatedFile.options.babelrc;
}
if (babelrcRoots === undefined) {
babelrcRootsDirectory = validatedFile.dirname;
babelrcRoots = validatedFile.options.babelrcRoots;
}
mergeChain(configFileChain, result);
}
let ignoreFile, babelrcFile;
let isIgnored = false;
const fileChain = emptyChain();
if ((babelrc === true || babelrc === undefined) && typeof context.filename === "string") {
const pkgData = yield* (0, _index.findPackageData)(context.filename);
if (pkgData && babelrcLoadEnabled(context, pkgData, babelrcRoots, babelrcRootsDirectory)) {
({
ignore: ignoreFile,
config: babelrcFile
} = yield* (0, _index.findRelativeConfig)(pkgData, context.envName, context.caller));
if (ignoreFile) {
fileChain.files.add(ignoreFile.filepath);
}
if (ignoreFile && shouldIgnore(context, ignoreFile.ignore, null, ignoreFile.dirname)) {
isIgnored = true;
}
if (babelrcFile && !isIgnored) {
const validatedFile = validateBabelrcFile(babelrcFile);
const babelrcLogger = new _printer.ConfigPrinter();
const result = yield* loadFileChain(validatedFile, context, undefined, babelrcLogger);
if (!result) {
isIgnored = true;
} else {
babelRcReport = yield* babelrcLogger.output();
mergeChain(fileChain, result);
}
}
if (babelrcFile && isIgnored) {
fileChain.files.add(babelrcFile.filepath);
}
}
}
if (context.showConfig) {
console.log(`Babel configs on "${context.filename}" (ascending priority):\n` + [configReport, babelRcReport, programmaticReport].filter(x => !!x).join("\n\n") + "\n-----End Babel configs-----");
}
const chain = mergeChain(mergeChain(mergeChain(emptyChain(), configFileChain), fileChain), programmaticChain);
return {
plugins: isIgnored ? [] : dedupDescriptors(chain.plugins),
presets: isIgnored ? [] : dedupDescriptors(chain.presets),
options: isIgnored ? [] : chain.options.map(o => normalizeOptions(o)),
fileHandling: isIgnored ? "ignored" : "transpile",
ignore: ignoreFile || undefined,
babelrc: babelrcFile || undefined,
config: configFile || undefined,
files: chain.files
};
}
function babelrcLoadEnabled(context, pkgData, babelrcRoots, babelrcRootsDirectory) {
if (typeof babelrcRoots === "boolean") return babelrcRoots;
const absoluteRoot = context.root;
if (babelrcRoots === undefined) {
return pkgData.directories.includes(absoluteRoot);
}
let babelrcPatterns = babelrcRoots;
if (!Array.isArray(babelrcPatterns)) {
babelrcPatterns = [babelrcPatterns];
}
babelrcPatterns = babelrcPatterns.map(pat => {
return typeof pat === "string" ? _path().resolve(babelrcRootsDirectory, pat) : pat;
});
if (babelrcPatterns.length === 1 && babelrcPatterns[0] === absoluteRoot) {
return pkgData.directories.includes(absoluteRoot);
}
return babelrcPatterns.some(pat => {
if (typeof pat === "string") {
pat = (0, _patternToRegex.default)(pat, babelrcRootsDirectory);
}
return pkgData.directories.some(directory => {
return matchPattern(pat, babelrcRootsDirectory, directory, context);
});
});
}
const validateConfigFile = (0, _caching.makeWeakCacheSync)(file => ({
filepath: file.filepath,
dirname: file.dirname,
options: (0, _options.validate)("configfile", file.options, file.filepath)
}));
const validateBabelrcFile = (0, _caching.makeWeakCacheSync)(file => ({
filepath: file.filepath,
dirname: file.dirname,
options: (0, _options.validate)("babelrcfile", file.options, file.filepath)
}));
const validateExtendFile = (0, _caching.makeWeakCacheSync)(file => ({
filepath: file.filepath,
dirname: file.dirname,
options: (0, _options.validate)("extendsfile", file.options, file.filepath)
}));
const loadProgrammaticChain = makeChainWalker({
root: input => buildRootDescriptors(input, "base", _configDescriptors.createCachedDescriptors),
env: (input, envName) => buildEnvDescriptors(input, "base", _configDescriptors.createCachedDescriptors, envName),
overrides: (input, index) => buildOverrideDescriptors(input, "base", _configDescriptors.createCachedDescriptors, index),
overridesEnv: (input, index, envName) => buildOverrideEnvDescriptors(input, "base", _configDescriptors.createCachedDescriptors, index, envName),
createLogger: (input, context, baseLogger) => buildProgrammaticLogger(input, context, baseLogger)
});
const loadFileChainWalker = makeChainWalker({
root: file => loadFileDescriptors(file),
env: (file, envName) => loadFileEnvDescriptors(file)(envName),
overrides: (file, index) => loadFileOverridesDescriptors(file)(index),
overridesEnv: (file, index, envName) => loadFileOverridesEnvDescriptors(file)(index)(envName),
createLogger: (file, context, baseLogger) => buildFileLogger(file.filepath, context, baseLogger)
});
function* loadFileChain(input, context, files, baseLogger) {
const chain = yield* loadFileChainWalker(input, context, files, baseLogger);
chain == null || chain.files.add(input.filepath);
return chain;
}
const loadFileDescriptors = (0, _caching.makeWeakCacheSync)(file => buildRootDescriptors(file, file.filepath, _configDescriptors.createUncachedDescriptors));
const loadFileEnvDescriptors = (0, _caching.makeWeakCacheSync)(file => (0, _caching.makeStrongCacheSync)(envName => buildEnvDescriptors(file, file.filepath, _configDescriptors.createUncachedDescriptors, envName)));
const loadFileOverridesDescriptors = (0, _caching.makeWeakCacheSync)(file => (0, _caching.makeStrongCacheSync)(index => buildOverrideDescriptors(file, file.filepath, _configDescriptors.createUncachedDescriptors, index)));
const loadFileOverridesEnvDescriptors = (0, _caching.makeWeakCacheSync)(file => (0, _caching.makeStrongCacheSync)(index => (0, _caching.makeStrongCacheSync)(envName => buildOverrideEnvDescriptors(file, file.filepath, _configDescriptors.createUncachedDescriptors, index, envName))));
function buildFileLogger(filepath, context, baseLogger) {
if (!baseLogger) {
return () => {};
}
return baseLogger.configure(context.showConfig, _printer.ChainFormatter.Config, {
filepath
});
}
function buildRootDescriptors({
dirname,
options
}, alias, descriptors) {
return descriptors(dirname, options, alias);
}
function buildProgrammaticLogger(_, context, baseLogger) {
var _context$caller;
if (!baseLogger) {
return () => {};
}
return baseLogger.configure(context.showConfig, _printer.ChainFormatter.Programmatic, {
callerName: (_context$caller = context.caller) == null ? void 0 : _context$caller.name
});
}
function buildEnvDescriptors({
dirname,
options
}, alias, descriptors, envName) {
var _options$env;
const opts = (_options$env = options.env) == null ? void 0 : _options$env[envName];
return opts ? descriptors(dirname, opts, `${alias}.env["${envName}"]`) : null;
}
function buildOverrideDescriptors({
dirname,
options
}, alias, descriptors, index) {
var _options$overrides;
const opts = (_options$overrides = options.overrides) == null ? void 0 : _options$overrides[index];
if (!opts) throw new Error("Assertion failure - missing override");
return descriptors(dirname, opts, `${alias}.overrides[${index}]`);
}
function buildOverrideEnvDescriptors({
dirname,
options
}, alias, descriptors, index, envName) {
var _options$overrides2, _override$env;
const override = (_options$overrides2 = options.overrides) == null ? void 0 : _options$overrides2[index];
if (!override) throw new Error("Assertion failure - missing override");
const opts = (_override$env = override.env) == null ? void 0 : _override$env[envName];
return opts ? descriptors(dirname, opts, `${alias}.overrides[${index}].env["${envName}"]`) : null;
}
function makeChainWalker({
root,
env,
overrides,
overridesEnv,
createLogger
}) {
return function* chainWalker(input, context, files = new Set(), baseLogger) {
const {
dirname
} = input;
const flattenedConfigs = [];
const rootOpts = root(input);
if (configIsApplicable(rootOpts, dirname, context, input.filepath)) {
flattenedConfigs.push({
config: rootOpts,
envName: undefined,
index: undefined
});
const envOpts = env(input, context.envName);
if (envOpts && configIsApplicable(envOpts, dirname, context, input.filepath)) {
flattenedConfigs.push({
config: envOpts,
envName: context.envName,
index: undefined
});
}
(rootOpts.options.overrides || []).forEach((_, index) => {
const overrideOps = overrides(input, index);
if (configIsApplicable(overrideOps, dirname, context, input.filepath)) {
flattenedConfigs.push({
config: overrideOps,
index,
envName: undefined
});
const overrideEnvOpts = overridesEnv(input, index, context.envName);
if (overrideEnvOpts && configIsApplicable(overrideEnvOpts, dirname, context, input.filepath)) {
flattenedConfigs.push({
config: overrideEnvOpts,
index,
envName: context.envName
});
}
}
});
}
if (flattenedConfigs.some(({
config: {
options: {
ignore,
only
}
}
}) => shouldIgnore(context, ignore, only, dirname))) {
return null;
}
const chain = emptyChain();
const logger = createLogger(input, context, baseLogger);
for (const {
config,
index,
envName
} of flattenedConfigs) {
if (!(yield* mergeExtendsChain(chain, config.options, dirname, context, files, baseLogger))) {
return null;
}
logger(config, index, envName);
yield* mergeChainOpts(chain, config);
}
return chain;
};
}
function* mergeExtendsChain(chain, opts, dirname, context, files, baseLogger) {
if (opts.extends === undefined) return true;
const file = yield* (0, _index.loadConfig)(opts.extends, dirname, context.envName, context.caller);
if (files.has(file)) {
throw new Error(`Configuration cycle detected loading ${file.filepath}.\n` + `File already loaded following the config chain:\n` + Array.from(files, file => ` - ${file.filepath}`).join("\n"));
}
files.add(file);
const fileChain = yield* loadFileChain(validateExtendFile(file), context, files, baseLogger);
files.delete(file);
if (!fileChain) return false;
mergeChain(chain, fileChain);
return true;
}
function mergeChain(target, source) {
target.options.push(...source.options);
target.plugins.push(...source.plugins);
target.presets.push(...source.presets);
for (const file of source.files) {
target.files.add(file);
}
return target;
}
function* mergeChainOpts(target, {
options,
plugins,
presets
}) {
target.options.push(options);
target.plugins.push(...(yield* plugins()));
target.presets.push(...(yield* presets()));
return target;
}
function emptyChain() {
return {
options: [],
presets: [],
plugins: [],
files: new Set()
};
}
function normalizeOptions(opts) {
const options = Object.assign({}, opts);
delete options.extends;
delete options.env;
delete options.overrides;
delete options.plugins;
delete options.presets;
delete options.passPerPreset;
delete options.ignore;
delete options.only;
delete options.test;
delete options.include;
delete options.exclude;
if (hasOwnProperty.call(options, "sourceMap")) {
options.sourceMaps = options.sourceMap;
delete options.sourceMap;
}
return options;
}
function dedupDescriptors(items) {
const map = new Map();
const descriptors = [];
for (const item of items) {
if (typeof item.value === "function") {
const fnKey = item.value;
let nameMap = map.get(fnKey);
if (!nameMap) {
nameMap = new Map();
map.set(fnKey, nameMap);
}
let desc = nameMap.get(item.name);
if (!desc) {
desc = {
value: item
};
descriptors.push(desc);
if (!item.ownPass) nameMap.set(item.name, desc);
} else {
desc.value = item;
}
} else {
descriptors.push({
value: item
});
}
}
return descriptors.reduce((acc, desc) => {
acc.push(desc.value);
return acc;
}, []);
}
function configIsApplicable({
options
}, dirname, context, configName) {
return (options.test === undefined || configFieldIsApplicable(context, options.test, dirname, configName)) && (options.include === undefined || configFieldIsApplicable(context, options.include, dirname, configName)) && (options.exclude === undefined || !configFieldIsApplicable(context, options.exclude, dirname, configName));
}
function configFieldIsApplicable(context, test, dirname, configName) {
const patterns = Array.isArray(test) ? test : [test];
return matchesPatterns(context, patterns, dirname, configName);
}
function ignoreListReplacer(_key, value) {
if (value instanceof RegExp) {
return String(value);
}
return value;
}
function shouldIgnore(context, ignore, only, dirname) {
if (ignore && matchesPatterns(context, ignore, dirname)) {
var _context$filename;
const message = `No config is applied to "${(_context$filename = context.filename) != null ? _context$filename : "(unknown)"}" because it matches one of \`ignore: ${JSON.stringify(ignore, ignoreListReplacer)}\` from "${dirname}"`;
debug(message);
if (context.showConfig) {
console.log(message);
}
return true;
}
if (only && !matchesPatterns(context, only, dirname)) {
var _context$filename2;
const message = `No config is applied to "${(_context$filename2 = context.filename) != null ? _context$filename2 : "(unknown)"}" because it fails to match one of \`only: ${JSON.stringify(only, ignoreListReplacer)}\` from "${dirname}"`;
debug(message);
if (context.showConfig) {
console.log(message);
}
return true;
}
return false;
}
function matchesPatterns(context, patterns, dirname, configName) {
return patterns.some(pattern => matchPattern(pattern, dirname, context.filename, context, configName));
}
function matchPattern(pattern, dirname, pathToTest, context, configName) {
if (typeof pattern === "function") {
return !!(0, _rewriteStackTrace.endHiddenCallStack)(pattern)(pathToTest, {
dirname,
envName: context.envName,
caller: context.caller
});
}
if (typeof pathToTest !== "string") {
throw new _configError.default(`Configuration contains string/RegExp pattern, but no filename was passed to Babel`, configName);
}
if (typeof pattern === "string") {
pattern = (0, _patternToRegex.default)(pattern, dirname);
}
return pattern.test(pathToTest);
}
0 && 0;
//# sourceMappingURL=config-chain.js.map

File diff suppressed because one or more lines are too long

View File

@@ -1,190 +0,0 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.createCachedDescriptors = createCachedDescriptors;
exports.createDescriptor = createDescriptor;
exports.createUncachedDescriptors = createUncachedDescriptors;
function _gensync() {
const data = require("gensync");
_gensync = function () {
return data;
};
return data;
}
var _functional = require("../gensync-utils/functional.js");
var _index = require("./files/index.js");
var _item = require("./item.js");
var _caching = require("./caching.js");
var _resolveTargets = require("./resolve-targets.js");
function isEqualDescriptor(a, b) {
var _a$file, _b$file, _a$file2, _b$file2;
return a.name === b.name && a.value === b.value && a.options === b.options && a.dirname === b.dirname && a.alias === b.alias && a.ownPass === b.ownPass && ((_a$file = a.file) == null ? void 0 : _a$file.request) === ((_b$file = b.file) == null ? void 0 : _b$file.request) && ((_a$file2 = a.file) == null ? void 0 : _a$file2.resolved) === ((_b$file2 = b.file) == null ? void 0 : _b$file2.resolved);
}
function* handlerOf(value) {
return value;
}
function optionsWithResolvedBrowserslistConfigFile(options, dirname) {
if (typeof options.browserslistConfigFile === "string") {
options.browserslistConfigFile = (0, _resolveTargets.resolveBrowserslistConfigFile)(options.browserslistConfigFile, dirname);
}
return options;
}
function createCachedDescriptors(dirname, options, alias) {
const {
plugins,
presets,
passPerPreset
} = options;
return {
options: optionsWithResolvedBrowserslistConfigFile(options, dirname),
plugins: plugins ? () => createCachedPluginDescriptors(plugins, dirname)(alias) : () => handlerOf([]),
presets: presets ? () => createCachedPresetDescriptors(presets, dirname)(alias)(!!passPerPreset) : () => handlerOf([])
};
}
function createUncachedDescriptors(dirname, options, alias) {
return {
options: optionsWithResolvedBrowserslistConfigFile(options, dirname),
plugins: (0, _functional.once)(() => createPluginDescriptors(options.plugins || [], dirname, alias)),
presets: (0, _functional.once)(() => createPresetDescriptors(options.presets || [], dirname, alias, !!options.passPerPreset))
};
}
const PRESET_DESCRIPTOR_CACHE = new WeakMap();
const createCachedPresetDescriptors = (0, _caching.makeWeakCacheSync)((items, cache) => {
const dirname = cache.using(dir => dir);
return (0, _caching.makeStrongCacheSync)(alias => (0, _caching.makeStrongCache)(function* (passPerPreset) {
const descriptors = yield* createPresetDescriptors(items, dirname, alias, passPerPreset);
return descriptors.map(desc => loadCachedDescriptor(PRESET_DESCRIPTOR_CACHE, desc));
}));
});
const PLUGIN_DESCRIPTOR_CACHE = new WeakMap();
const createCachedPluginDescriptors = (0, _caching.makeWeakCacheSync)((items, cache) => {
const dirname = cache.using(dir => dir);
return (0, _caching.makeStrongCache)(function* (alias) {
const descriptors = yield* createPluginDescriptors(items, dirname, alias);
return descriptors.map(desc => loadCachedDescriptor(PLUGIN_DESCRIPTOR_CACHE, desc));
});
});
const DEFAULT_OPTIONS = {};
function loadCachedDescriptor(cache, desc) {
const {
value,
options = DEFAULT_OPTIONS
} = desc;
if (options === false) return desc;
let cacheByOptions = cache.get(value);
if (!cacheByOptions) {
cacheByOptions = new WeakMap();
cache.set(value, cacheByOptions);
}
let possibilities = cacheByOptions.get(options);
if (!possibilities) {
possibilities = [];
cacheByOptions.set(options, possibilities);
}
if (!possibilities.includes(desc)) {
const matches = possibilities.filter(possibility => isEqualDescriptor(possibility, desc));
if (matches.length > 0) {
return matches[0];
}
possibilities.push(desc);
}
return desc;
}
function* createPresetDescriptors(items, dirname, alias, passPerPreset) {
return yield* createDescriptors("preset", items, dirname, alias, passPerPreset);
}
function* createPluginDescriptors(items, dirname, alias) {
return yield* createDescriptors("plugin", items, dirname, alias);
}
function* createDescriptors(type, items, dirname, alias, ownPass) {
const descriptors = yield* _gensync().all(items.map((item, index) => createDescriptor(item, dirname, {
type,
alias: `${alias}$${index}`,
ownPass: !!ownPass
})));
assertNoDuplicates(descriptors);
return descriptors;
}
function* createDescriptor(pair, dirname, {
type,
alias,
ownPass
}) {
const desc = (0, _item.getItemDescriptor)(pair);
if (desc) {
return desc;
}
let name;
let options;
let value = pair;
if (Array.isArray(value)) {
if (value.length === 3) {
[value, options, name] = value;
} else {
[value, options] = value;
}
}
let file = undefined;
let filepath = null;
if (typeof value === "string") {
if (typeof type !== "string") {
throw new Error("To resolve a string-based item, the type of item must be given");
}
const resolver = type === "plugin" ? _index.loadPlugin : _index.loadPreset;
const request = value;
({
filepath,
value
} = yield* resolver(value, dirname));
file = {
request,
resolved: filepath
};
}
if (!value) {
throw new Error(`Unexpected falsy value: ${String(value)}`);
}
if (typeof value === "object" && value.__esModule) {
if (value.default) {
value = value.default;
} else {
throw new Error("Must export a default export when using ES6 modules.");
}
}
if (typeof value !== "object" && typeof value !== "function") {
throw new Error(`Unsupported format: ${typeof value}. Expected an object or a function.`);
}
if (filepath !== null && typeof value === "object" && value) {
throw new Error(`Plugin/Preset files are not allowed to export objects, only functions. In ${filepath}`);
}
return {
name,
alias: filepath || alias,
value,
options,
dirname,
ownPass,
file
};
}
function assertNoDuplicates(items) {
const map = new Map();
for (const item of items) {
if (typeof item.value !== "function") continue;
let nameMap = map.get(item.value);
if (!nameMap) {
nameMap = new Set();
map.set(item.value, nameMap);
}
if (nameMap.has(item.name)) {
const conflicts = items.filter(i => i.value === item.value);
throw new Error([`Duplicate plugin/preset detected.`, `If you'd like to use two separate instances of a plugin,`, `they need separate names, e.g.`, ``, ` plugins: [`, ` ['some-plugin', {}],`, ` ['some-plugin', {}, 'some unique name'],`, ` ]`, ``, `Duplicates detected are:`, `${JSON.stringify(conflicts, null, 2)}`].join("\n"));
}
nameMap.add(item.name);
}
}
0 && 0;
//# sourceMappingURL=config-descriptors.js.map

File diff suppressed because one or more lines are too long

View File

@@ -1,290 +0,0 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.ROOT_CONFIG_FILENAMES = void 0;
exports.findConfigUpwards = findConfigUpwards;
exports.findRelativeConfig = findRelativeConfig;
exports.findRootConfig = findRootConfig;
exports.loadConfig = loadConfig;
exports.resolveShowConfigPath = resolveShowConfigPath;
function _debug() {
const data = require("debug");
_debug = function () {
return data;
};
return data;
}
function _fs() {
const data = require("fs");
_fs = function () {
return data;
};
return data;
}
function _path() {
const data = require("path");
_path = function () {
return data;
};
return data;
}
function _json() {
const data = require("json5");
_json = function () {
return data;
};
return data;
}
function _gensync() {
const data = require("gensync");
_gensync = function () {
return data;
};
return data;
}
var _caching = require("../caching.js");
var _configApi = require("../helpers/config-api.js");
var _utils = require("./utils.js");
var _moduleTypes = require("./module-types.js");
var _patternToRegex = require("../pattern-to-regex.js");
var _configError = require("../../errors/config-error.js");
var fs = require("../../gensync-utils/fs.js");
require("module");
var _rewriteStackTrace = require("../../errors/rewrite-stack-trace.js");
var _async = require("../../gensync-utils/async.js");
const debug = _debug()("babel:config:loading:files:configuration");
const ROOT_CONFIG_FILENAMES = exports.ROOT_CONFIG_FILENAMES = ["babel.config.js", "babel.config.cjs", "babel.config.mjs", "babel.config.json", "babel.config.cts", "babel.config.ts", "babel.config.mts"];
const RELATIVE_CONFIG_FILENAMES = [".babelrc", ".babelrc.js", ".babelrc.cjs", ".babelrc.mjs", ".babelrc.json", ".babelrc.cts"];
const BABELIGNORE_FILENAME = ".babelignore";
const runConfig = (0, _caching.makeWeakCache)(function* runConfig(options, cache) {
yield* [];
return {
options: (0, _rewriteStackTrace.endHiddenCallStack)(options)((0, _configApi.makeConfigAPI)(cache)),
cacheNeedsConfiguration: !cache.configured()
};
});
function* readConfigCode(filepath, data) {
if (!_fs().existsSync(filepath)) return null;
let options = yield* (0, _moduleTypes.default)(filepath, (yield* (0, _async.isAsync)()) ? "auto" : "require", "You appear to be using a native ECMAScript module configuration " + "file, which is only supported when running Babel asynchronously " + "or when using the Node.js `--experimental-require-module` flag.", "You appear to be using a configuration file that contains top-level " + "await, which is only supported when running Babel asynchronously.");
let cacheNeedsConfiguration = false;
if (typeof options === "function") {
({
options,
cacheNeedsConfiguration
} = yield* runConfig(options, data));
}
if (!options || typeof options !== "object" || Array.isArray(options)) {
throw new _configError.default(`Configuration should be an exported JavaScript object.`, filepath);
}
if (typeof options.then === "function") {
options.catch == null || options.catch(() => {});
throw new _configError.default(`You appear to be using an async configuration, ` + `which your current version of Babel does not support. ` + `We may add support for this in the future, ` + `but if you're on the most recent version of @babel/core and still ` + `seeing this error, then you'll need to synchronously return your config.`, filepath);
}
if (cacheNeedsConfiguration) throwConfigError(filepath);
return buildConfigFileObject(options, filepath);
}
const cfboaf = new WeakMap();
function buildConfigFileObject(options, filepath) {
let configFilesByFilepath = cfboaf.get(options);
if (!configFilesByFilepath) {
cfboaf.set(options, configFilesByFilepath = new Map());
}
let configFile = configFilesByFilepath.get(filepath);
if (!configFile) {
configFile = {
filepath,
dirname: _path().dirname(filepath),
options
};
configFilesByFilepath.set(filepath, configFile);
}
return configFile;
}
const packageToBabelConfig = (0, _caching.makeWeakCacheSync)(file => {
const babel = file.options.babel;
if (babel === undefined) return null;
if (typeof babel !== "object" || Array.isArray(babel) || babel === null) {
throw new _configError.default(`.babel property must be an object`, file.filepath);
}
return {
filepath: file.filepath,
dirname: file.dirname,
options: babel
};
});
const readConfigJSON5 = (0, _utils.makeStaticFileCache)((filepath, content) => {
let options;
try {
options = _json().parse(content);
} catch (err) {
throw new _configError.default(`Error while parsing config - ${err.message}`, filepath);
}
if (!options) throw new _configError.default(`No config detected`, filepath);
if (typeof options !== "object") {
throw new _configError.default(`Config returned typeof ${typeof options}`, filepath);
}
if (Array.isArray(options)) {
throw new _configError.default(`Expected config object but found array`, filepath);
}
delete options.$schema;
return {
filepath,
dirname: _path().dirname(filepath),
options
};
});
const readIgnoreConfig = (0, _utils.makeStaticFileCache)((filepath, content) => {
const ignoreDir = _path().dirname(filepath);
const ignorePatterns = content.split("\n").map(line => line.replace(/#.*$/, "").trim()).filter(Boolean);
for (const pattern of ignorePatterns) {
if (pattern[0] === "!") {
throw new _configError.default(`Negation of file paths is not supported.`, filepath);
}
}
return {
filepath,
dirname: _path().dirname(filepath),
ignore: ignorePatterns.map(pattern => (0, _patternToRegex.default)(pattern, ignoreDir))
};
});
function findConfigUpwards(rootDir) {
let dirname = rootDir;
for (;;) {
for (const filename of ROOT_CONFIG_FILENAMES) {
if (_fs().existsSync(_path().join(dirname, filename))) {
return dirname;
}
}
const nextDir = _path().dirname(dirname);
if (dirname === nextDir) break;
dirname = nextDir;
}
return null;
}
function* findRelativeConfig(packageData, envName, caller) {
let config = null;
let ignore = null;
const dirname = _path().dirname(packageData.filepath);
for (const loc of packageData.directories) {
if (!config) {
var _packageData$pkg;
config = yield* loadOneConfig(RELATIVE_CONFIG_FILENAMES, loc, envName, caller, ((_packageData$pkg = packageData.pkg) == null ? void 0 : _packageData$pkg.dirname) === loc ? packageToBabelConfig(packageData.pkg) : null);
}
if (!ignore) {
const ignoreLoc = _path().join(loc, BABELIGNORE_FILENAME);
ignore = yield* readIgnoreConfig(ignoreLoc);
if (ignore) {
debug("Found ignore %o from %o.", ignore.filepath, dirname);
}
}
}
return {
config,
ignore
};
}
function findRootConfig(dirname, envName, caller) {
return loadOneConfig(ROOT_CONFIG_FILENAMES, dirname, envName, caller);
}
function* loadOneConfig(names, dirname, envName, caller, previousConfig = null) {
const configs = yield* _gensync().all(names.map(filename => readConfig(_path().join(dirname, filename), envName, caller)));
const config = configs.reduce((previousConfig, config) => {
if (config && previousConfig) {
throw new _configError.default(`Multiple configuration files found. Please remove one:\n` + ` - ${_path().basename(previousConfig.filepath)}\n` + ` - ${config.filepath}\n` + `from ${dirname}`);
}
return config || previousConfig;
}, previousConfig);
if (config) {
debug("Found configuration %o from %o.", config.filepath, dirname);
}
return config;
}
function* loadConfig(name, dirname, envName, caller) {
const filepath = (((v, w) => (v = v.split("."), w = w.split("."), +v[0] > +w[0] || v[0] == w[0] && +v[1] >= +w[1]))(process.versions.node, "8.9") ? require.resolve : (r, {
paths: [b]
}, M = require("module")) => {
let f = M._findPath(r, M._nodeModulePaths(b).concat(b));
if (f) return f;
f = new Error(`Cannot resolve module '${r}'`);
f.code = "MODULE_NOT_FOUND";
throw f;
})(name, {
paths: [dirname]
});
const conf = yield* readConfig(filepath, envName, caller);
if (!conf) {
throw new _configError.default(`Config file contains no configuration data`, filepath);
}
debug("Loaded config %o from %o.", name, dirname);
return conf;
}
function readConfig(filepath, envName, caller) {
const ext = _path().extname(filepath);
switch (ext) {
case ".js":
case ".cjs":
case ".mjs":
case ".ts":
case ".cts":
case ".mts":
return readConfigCode(filepath, {
envName,
caller
});
default:
return readConfigJSON5(filepath);
}
}
function* resolveShowConfigPath(dirname) {
const targetPath = process.env.BABEL_SHOW_CONFIG_FOR;
if (targetPath != null) {
const absolutePath = _path().resolve(dirname, targetPath);
const stats = yield* fs.stat(absolutePath);
if (!stats.isFile()) {
throw new Error(`${absolutePath}: BABEL_SHOW_CONFIG_FOR must refer to a regular file, directories are not supported.`);
}
return absolutePath;
}
return null;
}
function throwConfigError(filepath) {
throw new _configError.default(`\
Caching was left unconfigured. Babel's plugins, presets, and .babelrc.js files can be configured
for various types of caching, using the first param of their handler functions:
module.exports = function(api) {
// The API exposes the following:
// Cache the returned value forever and don't call this function again.
api.cache(true);
// Don't cache at all. Not recommended because it will be very slow.
api.cache(false);
// Cached based on the value of some function. If this function returns a value different from
// a previously-encountered value, the plugins will re-evaluate.
var env = api.cache(() => process.env.NODE_ENV);
// If testing for a specific env, we recommend specifics to avoid instantiating a plugin for
// any possible NODE_ENV value that might come up during plugin execution.
var isProd = api.cache(() => process.env.NODE_ENV === "production");
// .cache(fn) will perform a linear search though instances to find the matching plugin based
// based on previous instantiated plugins. If you want to recreate the plugin and discard the
// previous instance whenever something changes, you may use:
var isProd = api.cache.invalidate(() => process.env.NODE_ENV === "production");
// Note, we also expose the following more-verbose versions of the above examples:
api.cache.forever(); // api.cache(true)
api.cache.never(); // api.cache(false)
api.cache.using(fn); // api.cache(fn)
// Return the value that will be cached.
return { };
};`, filepath);
}
0 && 0;
//# sourceMappingURL=configuration.js.map

File diff suppressed because one or more lines are too long

View File

@@ -1,6 +0,0 @@
module.exports = function import_(filepath) {
return import(filepath);
};
0 && 0;
//# sourceMappingURL=import.cjs.map

View File

@@ -1 +0,0 @@
{"version":3,"names":["module","exports","import_","filepath"],"sources":["../../../src/config/files/import.cjs"],"sourcesContent":["// We keep this in a separate file so that in older node versions, where\n// import() isn't supported, we can try/catch around the require() call\n// when loading this file.\n\nmodule.exports = function import_(filepath) {\n return import(filepath);\n};\n"],"mappings":"AAIAA,MAAM,CAACC,OAAO,GAAG,SAASC,OAAOA,CAACC,QAAQ,EAAE;EAC1C,OAAO,OAAOA,QAAQ,CAAC;AACzB,CAAC;AAAC","ignoreList":[]}

View File

@@ -1,58 +0,0 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.ROOT_CONFIG_FILENAMES = void 0;
exports.findConfigUpwards = findConfigUpwards;
exports.findPackageData = findPackageData;
exports.findRelativeConfig = findRelativeConfig;
exports.findRootConfig = findRootConfig;
exports.loadConfig = loadConfig;
exports.loadPlugin = loadPlugin;
exports.loadPreset = loadPreset;
exports.resolvePlugin = resolvePlugin;
exports.resolvePreset = resolvePreset;
exports.resolveShowConfigPath = resolveShowConfigPath;
function findConfigUpwards(rootDir) {
return null;
}
function* findPackageData(filepath) {
return {
filepath,
directories: [],
pkg: null,
isPackage: false
};
}
function* findRelativeConfig(pkgData, envName, caller) {
return {
config: null,
ignore: null
};
}
function* findRootConfig(dirname, envName, caller) {
return null;
}
function* loadConfig(name, dirname, envName, caller) {
throw new Error(`Cannot load ${name} relative to ${dirname} in a browser`);
}
function* resolveShowConfigPath(dirname) {
return null;
}
const ROOT_CONFIG_FILENAMES = exports.ROOT_CONFIG_FILENAMES = [];
function resolvePlugin(name, dirname) {
return null;
}
function resolvePreset(name, dirname) {
return null;
}
function loadPlugin(name, dirname) {
throw new Error(`Cannot load plugin ${name} relative to ${dirname} in a browser`);
}
function loadPreset(name, dirname) {
throw new Error(`Cannot load preset ${name} relative to ${dirname} in a browser`);
}
0 && 0;
//# sourceMappingURL=index-browser.js.map

View File

@@ -1 +0,0 @@
{"version":3,"names":["findConfigUpwards","rootDir","findPackageData","filepath","directories","pkg","isPackage","findRelativeConfig","pkgData","envName","caller","config","ignore","findRootConfig","dirname","loadConfig","name","Error","resolveShowConfigPath","ROOT_CONFIG_FILENAMES","exports","resolvePlugin","resolvePreset","loadPlugin","loadPreset"],"sources":["../../../src/config/files/index-browser.ts"],"sourcesContent":["/* c8 ignore start */\n\nimport type { Handler } from \"gensync\";\n\nimport type {\n ConfigFile,\n IgnoreFile,\n RelativeConfig,\n FilePackageData,\n} from \"./types.ts\";\n\nimport type { CallerMetadata } from \"../validation/options.ts\";\n\nexport type { ConfigFile, IgnoreFile, RelativeConfig, FilePackageData };\n\nexport function findConfigUpwards(\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n rootDir: string,\n): string | null {\n return null;\n}\n\n// eslint-disable-next-line require-yield\nexport function* findPackageData(filepath: string): Handler<FilePackageData> {\n return {\n filepath,\n directories: [],\n pkg: null,\n isPackage: false,\n };\n}\n\n// eslint-disable-next-line require-yield\nexport function* findRelativeConfig(\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n pkgData: FilePackageData,\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n envName: string,\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n caller: CallerMetadata | undefined,\n): Handler<RelativeConfig> {\n return { config: null, ignore: null };\n}\n\n// eslint-disable-next-line require-yield\nexport function* findRootConfig(\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n dirname: string,\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n envName: string,\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n caller: CallerMetadata | undefined,\n): Handler<ConfigFile | null> {\n return null;\n}\n\n// eslint-disable-next-line require-yield\nexport function* loadConfig(\n name: string,\n dirname: string,\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n envName: string,\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n caller: CallerMetadata | undefined,\n): Handler<ConfigFile> {\n throw new Error(`Cannot load ${name} relative to ${dirname} in a browser`);\n}\n\n// eslint-disable-next-line require-yield\nexport function* resolveShowConfigPath(\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n dirname: string,\n): Handler<string | null> {\n return null;\n}\n\nexport const ROOT_CONFIG_FILENAMES: string[] = [];\n\ntype Resolved =\n | { loader: \"require\"; filepath: string }\n | { loader: \"import\"; filepath: string };\n\n// eslint-disable-next-line @typescript-eslint/no-unused-vars\nexport function resolvePlugin(name: string, dirname: string): Resolved | null {\n return null;\n}\n\n// eslint-disable-next-line @typescript-eslint/no-unused-vars\nexport function resolvePreset(name: string, dirname: string): Resolved | null {\n return null;\n}\n\nexport function loadPlugin(\n name: string,\n dirname: string,\n): Handler<{\n filepath: string;\n value: unknown;\n}> {\n throw new Error(\n `Cannot load plugin ${name} relative to ${dirname} in a browser`,\n );\n}\n\nexport function loadPreset(\n name: string,\n dirname: string,\n): Handler<{\n filepath: string;\n value: unknown;\n}> {\n throw new Error(\n `Cannot load preset ${name} relative to ${dirname} in a browser`,\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;AAeO,SAASA,iBAAiBA,CAE/BC,OAAe,EACA;EACf,OAAO,IAAI;AACb;AAGO,UAAUC,eAAeA,CAACC,QAAgB,EAA4B;EAC3E,OAAO;IACLA,QAAQ;IACRC,WAAW,EAAE,EAAE;IACfC,GAAG,EAAE,IAAI;IACTC,SAAS,EAAE;EACb,CAAC;AACH;AAGO,UAAUC,kBAAkBA,CAEjCC,OAAwB,EAExBC,OAAe,EAEfC,MAAkC,EACT;EACzB,OAAO;IAAEC,MAAM,EAAE,IAAI;IAAEC,MAAM,EAAE;EAAK,CAAC;AACvC;AAGO,UAAUC,cAAcA,CAE7BC,OAAe,EAEfL,OAAe,EAEfC,MAAkC,EACN;EAC5B,OAAO,IAAI;AACb;AAGO,UAAUK,UAAUA,CACzBC,IAAY,EACZF,OAAe,EAEfL,OAAe,EAEfC,MAAkC,EACb;EACrB,MAAM,IAAIO,KAAK,CAAC,eAAeD,IAAI,gBAAgBF,OAAO,eAAe,CAAC;AAC5E;AAGO,UAAUI,qBAAqBA,CAEpCJ,OAAe,EACS;EACxB,OAAO,IAAI;AACb;AAEO,MAAMK,qBAA+B,GAAAC,OAAA,CAAAD,qBAAA,GAAG,EAAE;AAO1C,SAASE,aAAaA,CAACL,IAAY,EAAEF,OAAe,EAAmB;EAC5E,OAAO,IAAI;AACb;AAGO,SAASQ,aAAaA,CAACN,IAAY,EAAEF,OAAe,EAAmB;EAC5E,OAAO,IAAI;AACb;AAEO,SAASS,UAAUA,CACxBP,IAAY,EACZF,OAAe,EAId;EACD,MAAM,IAAIG,KAAK,CACb,sBAAsBD,IAAI,gBAAgBF,OAAO,eACnD,CAAC;AACH;AAEO,SAASU,UAAUA,CACxBR,IAAY,EACZF,OAAe,EAId;EACD,MAAM,IAAIG,KAAK,CACb,sBAAsBD,IAAI,gBAAgBF,OAAO,eACnD,CAAC;AACH;AAAC","ignoreList":[]}

View File

@@ -1,78 +0,0 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
Object.defineProperty(exports, "ROOT_CONFIG_FILENAMES", {
enumerable: true,
get: function () {
return _configuration.ROOT_CONFIG_FILENAMES;
}
});
Object.defineProperty(exports, "findConfigUpwards", {
enumerable: true,
get: function () {
return _configuration.findConfigUpwards;
}
});
Object.defineProperty(exports, "findPackageData", {
enumerable: true,
get: function () {
return _package.findPackageData;
}
});
Object.defineProperty(exports, "findRelativeConfig", {
enumerable: true,
get: function () {
return _configuration.findRelativeConfig;
}
});
Object.defineProperty(exports, "findRootConfig", {
enumerable: true,
get: function () {
return _configuration.findRootConfig;
}
});
Object.defineProperty(exports, "loadConfig", {
enumerable: true,
get: function () {
return _configuration.loadConfig;
}
});
Object.defineProperty(exports, "loadPlugin", {
enumerable: true,
get: function () {
return _plugins.loadPlugin;
}
});
Object.defineProperty(exports, "loadPreset", {
enumerable: true,
get: function () {
return _plugins.loadPreset;
}
});
Object.defineProperty(exports, "resolvePlugin", {
enumerable: true,
get: function () {
return _plugins.resolvePlugin;
}
});
Object.defineProperty(exports, "resolvePreset", {
enumerable: true,
get: function () {
return _plugins.resolvePreset;
}
});
Object.defineProperty(exports, "resolveShowConfigPath", {
enumerable: true,
get: function () {
return _configuration.resolveShowConfigPath;
}
});
var _package = require("./package.js");
var _configuration = require("./configuration.js");
var _plugins = require("./plugins.js");
({});
0 && 0;
//# sourceMappingURL=index.js.map

View File

@@ -1 +0,0 @@
{"version":3,"names":["_package","require","_configuration","_plugins"],"sources":["../../../src/config/files/index.ts"],"sourcesContent":["type indexBrowserType = typeof import(\"./index-browser\");\ntype indexType = typeof import(\"./index\");\n\n// Kind of gross, but essentially asserting that the exports of this module are the same as the\n// exports of index-browser, since this file may be replaced at bundle time with index-browser.\n({}) as any as indexBrowserType as indexType;\n\nexport { findPackageData } from \"./package.ts\";\n\nexport {\n findConfigUpwards,\n findRelativeConfig,\n findRootConfig,\n loadConfig,\n resolveShowConfigPath,\n ROOT_CONFIG_FILENAMES,\n} from \"./configuration.ts\";\nexport type {\n ConfigFile,\n IgnoreFile,\n RelativeConfig,\n FilePackageData,\n} from \"./types.ts\";\nexport {\n loadPlugin,\n loadPreset,\n resolvePlugin,\n resolvePreset,\n} from \"./plugins.ts\";\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAOA,IAAAA,QAAA,GAAAC,OAAA;AAEA,IAAAC,cAAA,GAAAD,OAAA;AAcA,IAAAE,QAAA,GAAAF,OAAA;AAlBA,CAAC,CAAC,CAAC;AAA0C","ignoreList":[]}

View File

@@ -1,211 +0,0 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = loadCodeDefault;
exports.supportsESM = void 0;
var _async = require("../../gensync-utils/async.js");
function _path() {
const data = require("path");
_path = function () {
return data;
};
return data;
}
function _url() {
const data = require("url");
_url = function () {
return data;
};
return data;
}
require("module");
function _semver() {
const data = require("semver");
_semver = function () {
return data;
};
return data;
}
function _debug() {
const data = require("debug");
_debug = function () {
return data;
};
return data;
}
var _rewriteStackTrace = require("../../errors/rewrite-stack-trace.js");
var _configError = require("../../errors/config-error.js");
var _transformFile = require("../../transform-file.js");
function asyncGeneratorStep(n, t, e, r, o, a, c) { try { var i = n[a](c), u = i.value; } catch (n) { return void e(n); } i.done ? t(u) : Promise.resolve(u).then(r, o); }
function _asyncToGenerator(n) { return function () { var t = this, e = arguments; return new Promise(function (r, o) { var a = n.apply(t, e); function _next(n) { asyncGeneratorStep(a, r, o, _next, _throw, "next", n); } function _throw(n) { asyncGeneratorStep(a, r, o, _next, _throw, "throw", n); } _next(void 0); }); }; }
const debug = _debug()("babel:config:loading:files:module-types");
{
try {
var import_ = require("./import.cjs");
} catch (_unused) {}
}
const supportsESM = exports.supportsESM = _semver().satisfies(process.versions.node, "^12.17 || >=13.2");
const LOADING_CJS_FILES = new Set();
function loadCjsDefault(filepath) {
if (LOADING_CJS_FILES.has(filepath)) {
debug("Auto-ignoring usage of config %o.", filepath);
return {};
}
let module;
try {
LOADING_CJS_FILES.add(filepath);
module = (0, _rewriteStackTrace.endHiddenCallStack)(require)(filepath);
} finally {
LOADING_CJS_FILES.delete(filepath);
}
{
return module != null && (module.__esModule || module[Symbol.toStringTag] === "Module") ? module.default || (arguments[1] ? module : undefined) : module;
}
}
const loadMjsFromPath = (0, _rewriteStackTrace.endHiddenCallStack)(function () {
var _loadMjsFromPath = _asyncToGenerator(function* (filepath) {
const url = (0, _url().pathToFileURL)(filepath).toString() + "?import";
{
if (!import_) {
throw new _configError.default("Internal error: Native ECMAScript modules aren't supported by this platform.\n", filepath);
}
return yield import_(url);
}
});
function loadMjsFromPath(_x) {
return _loadMjsFromPath.apply(this, arguments);
}
return loadMjsFromPath;
}());
const tsNotSupportedError = ext => `\
You are using a ${ext} config file, but Babel only supports transpiling .cts configs. Either:
- Use a .cts config file
- Update to Node.js 23.6.0, which has native TypeScript support
- Install tsx to transpile ${ext} files on the fly\
`;
const SUPPORTED_EXTENSIONS = {
".js": "unknown",
".mjs": "esm",
".cjs": "cjs",
".ts": "unknown",
".mts": "esm",
".cts": "cjs"
};
const asyncModules = new Set();
function* loadCodeDefault(filepath, loader, esmError, tlaError) {
let async;
const ext = _path().extname(filepath);
const isTS = ext === ".ts" || ext === ".cts" || ext === ".mts";
const type = SUPPORTED_EXTENSIONS[hasOwnProperty.call(SUPPORTED_EXTENSIONS, ext) ? ext : ".js"];
const pattern = `${loader} ${type}`;
switch (pattern) {
case "require cjs":
case "auto cjs":
if (isTS) {
return ensureTsSupport(filepath, ext, () => loadCjsDefault(filepath));
} else {
return loadCjsDefault(filepath, arguments[2]);
}
case "auto unknown":
case "require unknown":
case "require esm":
try {
if (isTS) {
return ensureTsSupport(filepath, ext, () => loadCjsDefault(filepath));
} else {
return loadCjsDefault(filepath, arguments[2]);
}
} catch (e) {
if (e.code === "ERR_REQUIRE_ASYNC_MODULE" || e.code === "ERR_REQUIRE_CYCLE_MODULE" && asyncModules.has(filepath)) {
asyncModules.add(filepath);
if (!(async != null ? async : async = yield* (0, _async.isAsync)())) {
throw new _configError.default(tlaError, filepath);
}
} else if (e.code === "ERR_REQUIRE_ESM" || type === "esm") {} else {
throw e;
}
}
case "auto esm":
if (async != null ? async : async = yield* (0, _async.isAsync)()) {
const promise = isTS ? ensureTsSupport(filepath, ext, () => loadMjsFromPath(filepath)) : loadMjsFromPath(filepath);
return (yield* (0, _async.waitFor)(promise)).default;
}
if (isTS) {
throw new _configError.default(tsNotSupportedError(ext), filepath);
} else {
throw new _configError.default(esmError, filepath);
}
default:
throw new Error("Internal Babel error: unreachable code.");
}
}
function ensureTsSupport(filepath, ext, callback) {
if (process.features.typescript || require.extensions[".ts"] || require.extensions[".cts"] || require.extensions[".mts"]) {
return callback();
}
if (ext !== ".cts") {
throw new _configError.default(tsNotSupportedError(ext), filepath);
}
const opts = {
babelrc: false,
configFile: false,
sourceType: "unambiguous",
sourceMaps: "inline",
sourceFileName: _path().basename(filepath),
presets: [[getTSPreset(filepath), Object.assign({
onlyRemoveTypeImports: true,
optimizeConstEnums: true
}, {
allowDeclareFields: true
})]]
};
let handler = function (m, filename) {
if (handler && filename.endsWith(".cts")) {
try {
return m._compile((0, _transformFile.transformFileSync)(filename, Object.assign({}, opts, {
filename
})).code, filename);
} catch (error) {
const packageJson = require("@babel/preset-typescript/package.json");
if (_semver().lt(packageJson.version, "7.21.4")) {
console.error("`.cts` configuration file failed to load, please try to update `@babel/preset-typescript`.");
}
throw error;
}
}
return require.extensions[".js"](m, filename);
};
require.extensions[ext] = handler;
try {
return callback();
} finally {
if (require.extensions[ext] === handler) delete require.extensions[ext];
handler = undefined;
}
}
function getTSPreset(filepath) {
try {
return require("@babel/preset-typescript");
} catch (error) {
if (error.code !== "MODULE_NOT_FOUND") throw error;
let message = "You appear to be using a .cts file as Babel configuration, but the `@babel/preset-typescript` package was not found: please install it!";
{
if (process.versions.pnp) {
message += `
If you are using Yarn Plug'n'Play, you may also need to add the following configuration to your .yarnrc.yml file:
packageExtensions:
\t"@babel/core@*":
\t\tpeerDependencies:
\t\t\t"@babel/preset-typescript": "*"
`;
}
}
throw new _configError.default(message, filepath);
}
}
0 && 0;
//# sourceMappingURL=module-types.js.map

File diff suppressed because one or more lines are too long

View File

@@ -1,61 +0,0 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.findPackageData = findPackageData;
function _path() {
const data = require("path");
_path = function () {
return data;
};
return data;
}
var _utils = require("./utils.js");
var _configError = require("../../errors/config-error.js");
const PACKAGE_FILENAME = "package.json";
const readConfigPackage = (0, _utils.makeStaticFileCache)((filepath, content) => {
let options;
try {
options = JSON.parse(content);
} catch (err) {
throw new _configError.default(`Error while parsing JSON - ${err.message}`, filepath);
}
if (!options) throw new Error(`${filepath}: No config detected`);
if (typeof options !== "object") {
throw new _configError.default(`Config returned typeof ${typeof options}`, filepath);
}
if (Array.isArray(options)) {
throw new _configError.default(`Expected config object but found array`, filepath);
}
return {
filepath,
dirname: _path().dirname(filepath),
options
};
});
function* findPackageData(filepath) {
let pkg = null;
const directories = [];
let isPackage = true;
let dirname = _path().dirname(filepath);
while (!pkg && _path().basename(dirname) !== "node_modules") {
directories.push(dirname);
pkg = yield* readConfigPackage(_path().join(dirname, PACKAGE_FILENAME));
const nextLoc = _path().dirname(dirname);
if (dirname === nextLoc) {
isPackage = false;
break;
}
dirname = nextLoc;
}
return {
filepath,
directories,
pkg,
isPackage
};
}
0 && 0;
//# sourceMappingURL=package.js.map

View File

@@ -1 +0,0 @@
{"version":3,"names":["_path","data","require","_utils","_configError","PACKAGE_FILENAME","readConfigPackage","makeStaticFileCache","filepath","content","options","JSON","parse","err","ConfigError","message","Error","Array","isArray","dirname","path","findPackageData","pkg","directories","isPackage","basename","push","join","nextLoc"],"sources":["../../../src/config/files/package.ts"],"sourcesContent":["import path from \"node:path\";\nimport type { Handler } from \"gensync\";\nimport { makeStaticFileCache } from \"./utils.ts\";\n\nimport type { ConfigFile, FilePackageData } from \"./types.ts\";\n\nimport ConfigError from \"../../errors/config-error.ts\";\n\nconst PACKAGE_FILENAME = \"package.json\";\n\nconst readConfigPackage = makeStaticFileCache(\n (filepath, content): ConfigFile => {\n let options;\n try {\n options = JSON.parse(content) as unknown;\n } catch (err) {\n throw new ConfigError(\n `Error while parsing JSON - ${err.message}`,\n filepath,\n );\n }\n\n if (!options) throw new Error(`${filepath}: No config detected`);\n\n if (typeof options !== \"object\") {\n throw new ConfigError(\n `Config returned typeof ${typeof options}`,\n filepath,\n );\n }\n if (Array.isArray(options)) {\n throw new ConfigError(`Expected config object but found array`, filepath);\n }\n\n return {\n filepath,\n dirname: path.dirname(filepath),\n options,\n };\n },\n);\n\n/**\n * Find metadata about the package that this file is inside of. Resolution\n * of Babel's config requires general package information to decide when to\n * search for .babelrc files\n */\nexport function* findPackageData(filepath: string): Handler<FilePackageData> {\n let pkg = null;\n const directories = [];\n let isPackage = true;\n\n let dirname = path.dirname(filepath);\n while (!pkg && path.basename(dirname) !== \"node_modules\") {\n directories.push(dirname);\n\n pkg = yield* readConfigPackage(path.join(dirname, PACKAGE_FILENAME));\n\n const nextLoc = path.dirname(dirname);\n if (dirname === nextLoc) {\n isPackage = false;\n break;\n }\n dirname = nextLoc;\n }\n\n return { filepath, directories, pkg, isPackage };\n}\n"],"mappings":";;;;;;AAAA,SAAAA,MAAA;EAAA,MAAAC,IAAA,GAAAC,OAAA;EAAAF,KAAA,YAAAA,CAAA;IAAA,OAAAC,IAAA;EAAA;EAAA,OAAAA,IAAA;AAAA;AAEA,IAAAE,MAAA,GAAAD,OAAA;AAIA,IAAAE,YAAA,GAAAF,OAAA;AAEA,MAAMG,gBAAgB,GAAG,cAAc;AAEvC,MAAMC,iBAAiB,GAAG,IAAAC,0BAAmB,EAC3C,CAACC,QAAQ,EAAEC,OAAO,KAAiB;EACjC,IAAIC,OAAO;EACX,IAAI;IACFA,OAAO,GAAGC,IAAI,CAACC,KAAK,CAACH,OAAO,CAAY;EAC1C,CAAC,CAAC,OAAOI,GAAG,EAAE;IACZ,MAAM,IAAIC,oBAAW,CACnB,8BAA8BD,GAAG,CAACE,OAAO,EAAE,EAC3CP,QACF,CAAC;EACH;EAEA,IAAI,CAACE,OAAO,EAAE,MAAM,IAAIM,KAAK,CAAC,GAAGR,QAAQ,sBAAsB,CAAC;EAEhE,IAAI,OAAOE,OAAO,KAAK,QAAQ,EAAE;IAC/B,MAAM,IAAII,oBAAW,CACnB,0BAA0B,OAAOJ,OAAO,EAAE,EAC1CF,QACF,CAAC;EACH;EACA,IAAIS,KAAK,CAACC,OAAO,CAACR,OAAO,CAAC,EAAE;IAC1B,MAAM,IAAII,oBAAW,CAAC,wCAAwC,EAAEN,QAAQ,CAAC;EAC3E;EAEA,OAAO;IACLA,QAAQ;IACRW,OAAO,EAAEC,MAAGA,CAAC,CAACD,OAAO,CAACX,QAAQ,CAAC;IAC/BE;EACF,CAAC;AACH,CACF,CAAC;AAOM,UAAUW,eAAeA,CAACb,QAAgB,EAA4B;EAC3E,IAAIc,GAAG,GAAG,IAAI;EACd,MAAMC,WAAW,GAAG,EAAE;EACtB,IAAIC,SAAS,GAAG,IAAI;EAEpB,IAAIL,OAAO,GAAGC,MAAGA,CAAC,CAACD,OAAO,CAACX,QAAQ,CAAC;EACpC,OAAO,CAACc,GAAG,IAAIF,MAAGA,CAAC,CAACK,QAAQ,CAACN,OAAO,CAAC,KAAK,cAAc,EAAE;IACxDI,WAAW,CAACG,IAAI,CAACP,OAAO,CAAC;IAEzBG,GAAG,GAAG,OAAOhB,iBAAiB,CAACc,MAAGA,CAAC,CAACO,IAAI,CAACR,OAAO,EAAEd,gBAAgB,CAAC,CAAC;IAEpE,MAAMuB,OAAO,GAAGR,MAAGA,CAAC,CAACD,OAAO,CAACA,OAAO,CAAC;IACrC,IAAIA,OAAO,KAAKS,OAAO,EAAE;MACvBJ,SAAS,GAAG,KAAK;MACjB;IACF;IACAL,OAAO,GAAGS,OAAO;EACnB;EAEA,OAAO;IAAEpB,QAAQ;IAAEe,WAAW;IAAED,GAAG;IAAEE;EAAU,CAAC;AAClD;AAAC","ignoreList":[]}

View File

@@ -1,230 +0,0 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.loadPlugin = loadPlugin;
exports.loadPreset = loadPreset;
exports.resolvePreset = exports.resolvePlugin = void 0;
function _debug() {
const data = require("debug");
_debug = function () {
return data;
};
return data;
}
function _path() {
const data = require("path");
_path = function () {
return data;
};
return data;
}
var _async = require("../../gensync-utils/async.js");
var _moduleTypes = require("./module-types.js");
function _url() {
const data = require("url");
_url = function () {
return data;
};
return data;
}
var _importMetaResolve = require("../../vendor/import-meta-resolve.js");
require("module");
function _fs() {
const data = require("fs");
_fs = function () {
return data;
};
return data;
}
const debug = _debug()("babel:config:loading:files:plugins");
const EXACT_RE = /^module:/;
const BABEL_PLUGIN_PREFIX_RE = /^(?!@|module:|[^/]+\/|babel-plugin-)/;
const BABEL_PRESET_PREFIX_RE = /^(?!@|module:|[^/]+\/|babel-preset-)/;
const BABEL_PLUGIN_ORG_RE = /^(@babel\/)(?!plugin-|[^/]+\/)/;
const BABEL_PRESET_ORG_RE = /^(@babel\/)(?!preset-|[^/]+\/)/;
const OTHER_PLUGIN_ORG_RE = /^(@(?!babel\/)[^/]+\/)(?![^/]*babel-plugin(?:-|\/|$)|[^/]+\/)/;
const OTHER_PRESET_ORG_RE = /^(@(?!babel\/)[^/]+\/)(?![^/]*babel-preset(?:-|\/|$)|[^/]+\/)/;
const OTHER_ORG_DEFAULT_RE = /^(@(?!babel$)[^/]+)$/;
const resolvePlugin = exports.resolvePlugin = resolveStandardizedName.bind(null, "plugin");
const resolvePreset = exports.resolvePreset = resolveStandardizedName.bind(null, "preset");
function* loadPlugin(name, dirname) {
const {
filepath,
loader
} = resolvePlugin(name, dirname, yield* (0, _async.isAsync)());
const value = yield* requireModule("plugin", loader, filepath);
debug("Loaded plugin %o from %o.", name, dirname);
return {
filepath,
value
};
}
function* loadPreset(name, dirname) {
const {
filepath,
loader
} = resolvePreset(name, dirname, yield* (0, _async.isAsync)());
const value = yield* requireModule("preset", loader, filepath);
debug("Loaded preset %o from %o.", name, dirname);
return {
filepath,
value
};
}
function standardizeName(type, name) {
if (_path().isAbsolute(name)) return name;
const isPreset = type === "preset";
return name.replace(isPreset ? BABEL_PRESET_PREFIX_RE : BABEL_PLUGIN_PREFIX_RE, `babel-${type}-`).replace(isPreset ? BABEL_PRESET_ORG_RE : BABEL_PLUGIN_ORG_RE, `$1${type}-`).replace(isPreset ? OTHER_PRESET_ORG_RE : OTHER_PLUGIN_ORG_RE, `$1babel-${type}-`).replace(OTHER_ORG_DEFAULT_RE, `$1/babel-${type}`).replace(EXACT_RE, "");
}
function* resolveAlternativesHelper(type, name) {
const standardizedName = standardizeName(type, name);
const {
error,
value
} = yield standardizedName;
if (!error) return value;
if (error.code !== "MODULE_NOT_FOUND") throw error;
if (standardizedName !== name && !(yield name).error) {
error.message += `\n- If you want to resolve "${name}", use "module:${name}"`;
}
if (!(yield standardizeName(type, "@babel/" + name)).error) {
error.message += `\n- Did you mean "@babel/${name}"?`;
}
const oppositeType = type === "preset" ? "plugin" : "preset";
if (!(yield standardizeName(oppositeType, name)).error) {
error.message += `\n- Did you accidentally pass a ${oppositeType} as a ${type}?`;
}
if (type === "plugin") {
const transformName = standardizedName.replace("-proposal-", "-transform-");
if (transformName !== standardizedName && !(yield transformName).error) {
error.message += `\n- Did you mean "${transformName}"?`;
}
}
error.message += `\n
Make sure that all the Babel plugins and presets you are using
are defined as dependencies or devDependencies in your package.json
file. It's possible that the missing plugin is loaded by a preset
you are using that forgot to add the plugin to its dependencies: you
can workaround this problem by explicitly adding the missing package
to your top-level package.json.
`;
throw error;
}
function tryRequireResolve(id, dirname) {
try {
if (dirname) {
return {
error: null,
value: (((v, w) => (v = v.split("."), w = w.split("."), +v[0] > +w[0] || v[0] == w[0] && +v[1] >= +w[1]))(process.versions.node, "8.9") ? require.resolve : (r, {
paths: [b]
}, M = require("module")) => {
let f = M._findPath(r, M._nodeModulePaths(b).concat(b));
if (f) return f;
f = new Error(`Cannot resolve module '${r}'`);
f.code = "MODULE_NOT_FOUND";
throw f;
})(id, {
paths: [dirname]
})
};
} else {
return {
error: null,
value: require.resolve(id)
};
}
} catch (error) {
return {
error,
value: null
};
}
}
function tryImportMetaResolve(id, options) {
try {
return {
error: null,
value: (0, _importMetaResolve.resolve)(id, options)
};
} catch (error) {
return {
error,
value: null
};
}
}
function resolveStandardizedNameForRequire(type, name, dirname) {
const it = resolveAlternativesHelper(type, name);
let res = it.next();
while (!res.done) {
res = it.next(tryRequireResolve(res.value, dirname));
}
return {
loader: "require",
filepath: res.value
};
}
function resolveStandardizedNameForImport(type, name, dirname) {
const parentUrl = (0, _url().pathToFileURL)(_path().join(dirname, "./babel-virtual-resolve-base.js")).href;
const it = resolveAlternativesHelper(type, name);
let res = it.next();
while (!res.done) {
res = it.next(tryImportMetaResolve(res.value, parentUrl));
}
return {
loader: "auto",
filepath: (0, _url().fileURLToPath)(res.value)
};
}
function resolveStandardizedName(type, name, dirname, allowAsync) {
if (!_moduleTypes.supportsESM || !allowAsync) {
return resolveStandardizedNameForRequire(type, name, dirname);
}
try {
const resolved = resolveStandardizedNameForImport(type, name, dirname);
if (!(0, _fs().existsSync)(resolved.filepath)) {
throw Object.assign(new Error(`Could not resolve "${name}" in file ${dirname}.`), {
type: "MODULE_NOT_FOUND"
});
}
return resolved;
} catch (e) {
try {
return resolveStandardizedNameForRequire(type, name, dirname);
} catch (e2) {
if (e.type === "MODULE_NOT_FOUND") throw e;
if (e2.type === "MODULE_NOT_FOUND") throw e2;
throw e;
}
}
}
{
var LOADING_MODULES = new Set();
}
function* requireModule(type, loader, name) {
{
if (!(yield* (0, _async.isAsync)()) && LOADING_MODULES.has(name)) {
throw new Error(`Reentrant ${type} detected trying to load "${name}". This module is not ignored ` + "and is trying to load itself while compiling itself, leading to a dependency cycle. " + 'We recommend adding it to your "ignore" list in your babelrc, or to a .babelignore.');
}
}
try {
{
LOADING_MODULES.add(name);
}
{
return yield* (0, _moduleTypes.default)(name, loader, `You appear to be using a native ECMAScript module ${type}, ` + "which is only supported when running Babel asynchronously " + "or when using the Node.js `--experimental-require-module` flag.", `You appear to be using a ${type} that contains top-level await, ` + "which is only supported when running Babel asynchronously.", true);
}
} catch (err) {
err.message = `[BABEL]: ${err.message} (While processing: ${name})`;
throw err;
} finally {
{
LOADING_MODULES.delete(name);
}
}
}
0 && 0;
//# sourceMappingURL=plugins.js.map

File diff suppressed because one or more lines are too long

View File

@@ -1,5 +0,0 @@
"use strict";
0 && 0;
//# sourceMappingURL=types.js.map

View File

@@ -1 +0,0 @@
{"version":3,"names":[],"sources":["../../../src/config/files/types.ts"],"sourcesContent":["import type { InputOptions } from \"../index.ts\";\n\nexport type ConfigFile = {\n filepath: string;\n dirname: string;\n options: InputOptions & { babel?: unknown };\n};\n\nexport type IgnoreFile = {\n filepath: string;\n dirname: string;\n ignore: Array<RegExp>;\n};\n\nexport type RelativeConfig = {\n // The actual config, either from package.json#babel, .babelrc, or\n // .babelrc.js, if there was one.\n config: ConfigFile | null;\n // The .babelignore, if there was one.\n ignore: IgnoreFile | null;\n};\n\nexport type FilePackageData = {\n // The file in the package.\n filepath: string;\n // Any ancestor directories of the file that are within the package.\n directories: Array<string>;\n // The contents of the package.json. May not be found if the package just\n // terminated at a node_modules folder without finding one.\n pkg: ConfigFile | null;\n // True if a package.json or node_modules folder was found while traversing\n // the directory structure.\n isPackage: boolean;\n};\n"],"mappings":"","ignoreList":[]}

View File

@@ -1,36 +0,0 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.makeStaticFileCache = makeStaticFileCache;
var _caching = require("../caching.js");
var fs = require("../../gensync-utils/fs.js");
function _fs2() {
const data = require("fs");
_fs2 = function () {
return data;
};
return data;
}
function makeStaticFileCache(fn) {
return (0, _caching.makeStrongCache)(function* (filepath, cache) {
const cached = cache.invalidate(() => fileMtime(filepath));
if (cached === null) {
return null;
}
return fn(filepath, yield* fs.readFile(filepath, "utf8"));
});
}
function fileMtime(filepath) {
if (!_fs2().existsSync(filepath)) return null;
try {
return +_fs2().statSync(filepath).mtime;
} catch (e) {
if (e.code !== "ENOENT" && e.code !== "ENOTDIR") throw e;
}
return null;
}
0 && 0;
//# sourceMappingURL=utils.js.map

View File

@@ -1 +0,0 @@
{"version":3,"names":["_caching","require","fs","_fs2","data","makeStaticFileCache","fn","makeStrongCache","filepath","cache","cached","invalidate","fileMtime","readFile","nodeFs","existsSync","statSync","mtime","e","code"],"sources":["../../../src/config/files/utils.ts"],"sourcesContent":["import type { Handler } from \"gensync\";\n\nimport { makeStrongCache } from \"../caching.ts\";\nimport type { CacheConfigurator } from \"../caching.ts\";\nimport * as fs from \"../../gensync-utils/fs.ts\";\nimport nodeFs from \"node:fs\";\n\nexport function makeStaticFileCache<T>(\n fn: (filepath: string, contents: string) => T,\n) {\n return makeStrongCache(function* (\n filepath: string,\n cache: CacheConfigurator<void>,\n ): Handler<null | T> {\n const cached = cache.invalidate(() => fileMtime(filepath));\n\n if (cached === null) {\n return null;\n }\n\n return fn(filepath, yield* fs.readFile(filepath, \"utf8\"));\n });\n}\n\nfunction fileMtime(filepath: string): number | null {\n if (!nodeFs.existsSync(filepath)) return null;\n\n try {\n return +nodeFs.statSync(filepath).mtime;\n } catch (e) {\n if (e.code !== \"ENOENT\" && e.code !== \"ENOTDIR\") throw e;\n }\n\n return null;\n}\n"],"mappings":";;;;;;AAEA,IAAAA,QAAA,GAAAC,OAAA;AAEA,IAAAC,EAAA,GAAAD,OAAA;AACA,SAAAE,KAAA;EAAA,MAAAC,IAAA,GAAAH,OAAA;EAAAE,IAAA,YAAAA,CAAA;IAAA,OAAAC,IAAA;EAAA;EAAA,OAAAA,IAAA;AAAA;AAEO,SAASC,mBAAmBA,CACjCC,EAA6C,EAC7C;EACA,OAAO,IAAAC,wBAAe,EAAC,WACrBC,QAAgB,EAChBC,KAA8B,EACX;IACnB,MAAMC,MAAM,GAAGD,KAAK,CAACE,UAAU,CAAC,MAAMC,SAAS,CAACJ,QAAQ,CAAC,CAAC;IAE1D,IAAIE,MAAM,KAAK,IAAI,EAAE;MACnB,OAAO,IAAI;IACb;IAEA,OAAOJ,EAAE,CAACE,QAAQ,EAAE,OAAON,EAAE,CAACW,QAAQ,CAACL,QAAQ,EAAE,MAAM,CAAC,CAAC;EAC3D,CAAC,CAAC;AACJ;AAEA,SAASI,SAASA,CAACJ,QAAgB,EAAiB;EAClD,IAAI,CAACM,KAAKA,CAAC,CAACC,UAAU,CAACP,QAAQ,CAAC,EAAE,OAAO,IAAI;EAE7C,IAAI;IACF,OAAO,CAACM,KAAKA,CAAC,CAACE,QAAQ,CAACR,QAAQ,CAAC,CAACS,KAAK;EACzC,CAAC,CAAC,OAAOC,CAAC,EAAE;IACV,IAAIA,CAAC,CAACC,IAAI,KAAK,QAAQ,IAAID,CAAC,CAACC,IAAI,KAAK,SAAS,EAAE,MAAMD,CAAC;EAC1D;EAEA,OAAO,IAAI;AACb;AAAC","ignoreList":[]}

View File

@@ -1,312 +0,0 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
function _gensync() {
const data = require("gensync");
_gensync = function () {
return data;
};
return data;
}
var _async = require("../gensync-utils/async.js");
var _util = require("./util.js");
var context = require("../index.js");
var _plugin = require("./plugin.js");
var _item = require("./item.js");
var _configChain = require("./config-chain.js");
var _deepArray = require("./helpers/deep-array.js");
function _traverse() {
const data = require("@babel/traverse");
_traverse = function () {
return data;
};
return data;
}
var _caching = require("./caching.js");
var _options = require("./validation/options.js");
var _plugins = require("./validation/plugins.js");
var _configApi = require("./helpers/config-api.js");
var _partial = require("./partial.js");
var _configError = require("../errors/config-error.js");
var _default = exports.default = _gensync()(function* loadFullConfig(inputOpts) {
var _opts$assumptions;
const result = yield* (0, _partial.default)(inputOpts);
if (!result) {
return null;
}
const {
options,
context,
fileHandling
} = result;
if (fileHandling === "ignored") {
return null;
}
const optionDefaults = {};
const {
plugins,
presets
} = options;
if (!plugins || !presets) {
throw new Error("Assertion failure - plugins and presets exist");
}
const presetContext = Object.assign({}, context, {
targets: options.targets
});
const toDescriptor = item => {
const desc = (0, _item.getItemDescriptor)(item);
if (!desc) {
throw new Error("Assertion failure - must be config item");
}
return desc;
};
const presetsDescriptors = presets.map(toDescriptor);
const initialPluginsDescriptors = plugins.map(toDescriptor);
const pluginDescriptorsByPass = [[]];
const passes = [];
const externalDependencies = [];
const ignored = yield* enhanceError(context, function* recursePresetDescriptors(rawPresets, pluginDescriptorsPass) {
const presets = [];
for (let i = 0; i < rawPresets.length; i++) {
const descriptor = rawPresets[i];
if (descriptor.options !== false) {
try {
var preset = yield* loadPresetDescriptor(descriptor, presetContext);
} catch (e) {
if (e.code === "BABEL_UNKNOWN_OPTION") {
(0, _options.checkNoUnwrappedItemOptionPairs)(rawPresets, i, "preset", e);
}
throw e;
}
externalDependencies.push(preset.externalDependencies);
if (descriptor.ownPass) {
presets.push({
preset: preset.chain,
pass: []
});
} else {
presets.unshift({
preset: preset.chain,
pass: pluginDescriptorsPass
});
}
}
}
if (presets.length > 0) {
pluginDescriptorsByPass.splice(1, 0, ...presets.map(o => o.pass).filter(p => p !== pluginDescriptorsPass));
for (const {
preset,
pass
} of presets) {
if (!preset) return true;
pass.push(...preset.plugins);
const ignored = yield* recursePresetDescriptors(preset.presets, pass);
if (ignored) return true;
preset.options.forEach(opts => {
(0, _util.mergeOptions)(optionDefaults, opts);
});
}
}
})(presetsDescriptors, pluginDescriptorsByPass[0]);
if (ignored) return null;
const opts = optionDefaults;
(0, _util.mergeOptions)(opts, options);
const pluginContext = Object.assign({}, presetContext, {
assumptions: (_opts$assumptions = opts.assumptions) != null ? _opts$assumptions : {}
});
yield* enhanceError(context, function* loadPluginDescriptors() {
pluginDescriptorsByPass[0].unshift(...initialPluginsDescriptors);
for (const descs of pluginDescriptorsByPass) {
const pass = [];
passes.push(pass);
for (let i = 0; i < descs.length; i++) {
const descriptor = descs[i];
if (descriptor.options !== false) {
try {
var plugin = yield* loadPluginDescriptor(descriptor, pluginContext);
} catch (e) {
if (e.code === "BABEL_UNKNOWN_PLUGIN_PROPERTY") {
(0, _options.checkNoUnwrappedItemOptionPairs)(descs, i, "plugin", e);
}
throw e;
}
pass.push(plugin);
externalDependencies.push(plugin.externalDependencies);
}
}
}
})();
opts.plugins = passes[0];
opts.presets = passes.slice(1).filter(plugins => plugins.length > 0).map(plugins => ({
plugins
}));
opts.passPerPreset = opts.presets.length > 0;
return {
options: opts,
passes: passes,
externalDependencies: (0, _deepArray.finalize)(externalDependencies)
};
});
function enhanceError(context, fn) {
return function* (arg1, arg2) {
try {
return yield* fn(arg1, arg2);
} catch (e) {
if (!/^\[BABEL\]/.test(e.message)) {
var _context$filename;
e.message = `[BABEL] ${(_context$filename = context.filename) != null ? _context$filename : "unknown file"}: ${e.message}`;
}
throw e;
}
};
}
const makeDescriptorLoader = apiFactory => (0, _caching.makeWeakCache)(function* ({
value,
options,
dirname,
alias
}, cache) {
if (options === false) throw new Error("Assertion failure");
options = options || {};
const externalDependencies = [];
let item = value;
if (typeof value === "function") {
const factory = (0, _async.maybeAsync)(value, `You appear to be using an async plugin/preset, but Babel has been called synchronously`);
const api = Object.assign({}, context, apiFactory(cache, externalDependencies));
try {
item = yield* factory(api, options, dirname);
} catch (e) {
if (alias) {
e.message += ` (While processing: ${JSON.stringify(alias)})`;
}
throw e;
}
}
if (!item || typeof item !== "object") {
throw new Error("Plugin/Preset did not return an object.");
}
if ((0, _async.isThenable)(item)) {
yield* [];
throw new Error(`You appear to be using a promise as a plugin, ` + `which your current version of Babel does not support. ` + `If you're using a published plugin, ` + `you may need to upgrade your @babel/core version. ` + `As an alternative, you can prefix the promise with "await". ` + `(While processing: ${JSON.stringify(alias)})`);
}
if (externalDependencies.length > 0 && (!cache.configured() || cache.mode() === "forever")) {
let error = `A plugin/preset has external untracked dependencies ` + `(${externalDependencies[0]}), but the cache `;
if (!cache.configured()) {
error += `has not been configured to be invalidated when the external dependencies change. `;
} else {
error += ` has been configured to never be invalidated. `;
}
error += `Plugins/presets should configure their cache to be invalidated when the external ` + `dependencies change, for example using \`api.cache.invalidate(() => ` + `statSync(filepath).mtimeMs)\` or \`api.cache.never()\`\n` + `(While processing: ${JSON.stringify(alias)})`;
throw new Error(error);
}
return {
value: item,
options,
dirname,
alias,
externalDependencies: (0, _deepArray.finalize)(externalDependencies)
};
});
const pluginDescriptorLoader = makeDescriptorLoader(_configApi.makePluginAPI);
const presetDescriptorLoader = makeDescriptorLoader(_configApi.makePresetAPI);
const instantiatePlugin = (0, _caching.makeWeakCache)(function* ({
value,
options,
dirname,
alias,
externalDependencies
}, cache) {
const pluginObj = (0, _plugins.validatePluginObject)(value);
const plugin = Object.assign({}, pluginObj);
if (plugin.visitor) {
plugin.visitor = _traverse().default.explode(Object.assign({}, plugin.visitor));
}
if (plugin.inherits) {
const inheritsDescriptor = {
name: undefined,
alias: `${alias}$inherits`,
value: plugin.inherits,
options,
dirname
};
const inherits = yield* (0, _async.forwardAsync)(loadPluginDescriptor, run => {
return cache.invalidate(data => run(inheritsDescriptor, data));
});
plugin.pre = chainMaybeAsync(inherits.pre, plugin.pre);
plugin.post = chainMaybeAsync(inherits.post, plugin.post);
plugin.manipulateOptions = chainMaybeAsync(inherits.manipulateOptions, plugin.manipulateOptions);
plugin.visitor = _traverse().default.visitors.merge([inherits.visitor || {}, plugin.visitor || {}]);
if (inherits.externalDependencies.length > 0) {
if (externalDependencies.length === 0) {
externalDependencies = inherits.externalDependencies;
} else {
externalDependencies = (0, _deepArray.finalize)([externalDependencies, inherits.externalDependencies]);
}
}
}
return new _plugin.default(plugin, options, alias, externalDependencies);
});
function* loadPluginDescriptor(descriptor, context) {
if (descriptor.value instanceof _plugin.default) {
if (descriptor.options) {
throw new Error("Passed options to an existing Plugin instance will not work.");
}
return descriptor.value;
}
return yield* instantiatePlugin(yield* pluginDescriptorLoader(descriptor, context), context);
}
const needsFilename = val => val && typeof val !== "function";
const validateIfOptionNeedsFilename = (options, descriptor) => {
if (needsFilename(options.test) || needsFilename(options.include) || needsFilename(options.exclude)) {
const formattedPresetName = descriptor.name ? `"${descriptor.name}"` : "/* your preset */";
throw new _configError.default([`Preset ${formattedPresetName} requires a filename to be set when babel is called directly,`, `\`\`\``, `babel.transformSync(code, { filename: 'file.ts', presets: [${formattedPresetName}] });`, `\`\`\``, `See https://babeljs.io/docs/en/options#filename for more information.`].join("\n"));
}
};
const validatePreset = (preset, context, descriptor) => {
if (!context.filename) {
var _options$overrides;
const {
options
} = preset;
validateIfOptionNeedsFilename(options, descriptor);
(_options$overrides = options.overrides) == null || _options$overrides.forEach(overrideOptions => validateIfOptionNeedsFilename(overrideOptions, descriptor));
}
};
const instantiatePreset = (0, _caching.makeWeakCacheSync)(({
value,
dirname,
alias,
externalDependencies
}) => {
return {
options: (0, _options.validate)("preset", value),
alias,
dirname,
externalDependencies
};
});
function* loadPresetDescriptor(descriptor, context) {
const preset = instantiatePreset(yield* presetDescriptorLoader(descriptor, context));
validatePreset(preset, context, descriptor);
return {
chain: yield* (0, _configChain.buildPresetChain)(preset, context),
externalDependencies: preset.externalDependencies
};
}
function chainMaybeAsync(a, b) {
if (!a) return b;
if (!b) return a;
return function (...args) {
const res = a.apply(this, args);
if (res && typeof res.then === "function") {
return res.then(() => b.apply(this, args));
}
return b.apply(this, args);
};
}
0 && 0;
//# sourceMappingURL=full.js.map

File diff suppressed because one or more lines are too long

View File

@@ -1,84 +0,0 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.makeConfigAPI = makeConfigAPI;
exports.makePluginAPI = makePluginAPI;
exports.makePresetAPI = makePresetAPI;
function _semver() {
const data = require("semver");
_semver = function () {
return data;
};
return data;
}
var _index = require("../../index.js");
var _caching = require("../caching.js");
function makeConfigAPI(cache) {
const env = value => cache.using(data => {
if (value === undefined) return data.envName;
if (typeof value === "function") {
return (0, _caching.assertSimpleType)(value(data.envName));
}
return (Array.isArray(value) ? value : [value]).some(entry => {
if (typeof entry !== "string") {
throw new Error("Unexpected non-string value");
}
return entry === data.envName;
});
});
const caller = cb => cache.using(data => (0, _caching.assertSimpleType)(cb(data.caller)));
return {
version: _index.version,
cache: cache.simple(),
env,
async: () => false,
caller,
assertVersion
};
}
function makePresetAPI(cache, externalDependencies) {
const targets = () => JSON.parse(cache.using(data => JSON.stringify(data.targets)));
const addExternalDependency = ref => {
externalDependencies.push(ref);
};
return Object.assign({}, makeConfigAPI(cache), {
targets,
addExternalDependency
});
}
function makePluginAPI(cache, externalDependencies) {
const assumption = name => cache.using(data => data.assumptions[name]);
return Object.assign({}, makePresetAPI(cache, externalDependencies), {
assumption
});
}
function assertVersion(range) {
if (typeof range === "number") {
if (!Number.isInteger(range)) {
throw new Error("Expected string or integer value.");
}
range = `^${range}.0.0-0`;
}
if (typeof range !== "string") {
throw new Error("Expected string or integer value.");
}
if (range === "*" || _semver().satisfies(_index.version, range)) return;
const limit = Error.stackTraceLimit;
if (typeof limit === "number" && limit < 25) {
Error.stackTraceLimit = 25;
}
const err = new Error(`Requires Babel "${range}", but was loaded with "${_index.version}". ` + `If you are sure you have a compatible version of @babel/core, ` + `it is likely that something in your build process is loading the ` + `wrong version. Inspect the stack trace of this error to look for ` + `the first entry that doesn't mention "@babel/core" or "babel-core" ` + `to see what is calling Babel.`);
if (typeof limit === "number") {
Error.stackTraceLimit = limit;
}
throw Object.assign(err, {
code: "BABEL_VERSION_UNSUPPORTED",
version: _index.version,
range
});
}
0 && 0;
//# sourceMappingURL=config-api.js.map

File diff suppressed because one or more lines are too long

View File

@@ -1,23 +0,0 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.finalize = finalize;
exports.flattenToSet = flattenToSet;
function finalize(deepArr) {
return Object.freeze(deepArr);
}
function flattenToSet(arr) {
const result = new Set();
const stack = [arr];
while (stack.length > 0) {
for (const el of stack.pop()) {
if (Array.isArray(el)) stack.push(el);else result.add(el);
}
}
return result;
}
0 && 0;
//# sourceMappingURL=deep-array.js.map

View File

@@ -1 +0,0 @@
{"version":3,"names":["finalize","deepArr","Object","freeze","flattenToSet","arr","result","Set","stack","length","el","pop","Array","isArray","push","add"],"sources":["../../../src/config/helpers/deep-array.ts"],"sourcesContent":["export type DeepArray<T> = Array<T | ReadonlyDeepArray<T>>;\n\n// Just to make sure that DeepArray<T> is not assignable to ReadonlyDeepArray<T>\ndeclare const __marker: unique symbol;\nexport type ReadonlyDeepArray<T> = ReadonlyArray<T | ReadonlyDeepArray<T>> & {\n [__marker]: true;\n};\n\nexport function finalize<T>(deepArr: DeepArray<T>): ReadonlyDeepArray<T> {\n return Object.freeze(deepArr) as ReadonlyDeepArray<T>;\n}\n\nexport function flattenToSet<T extends string>(\n arr: ReadonlyDeepArray<T>,\n): Set<T> {\n const result = new Set<T>();\n const stack = [arr];\n while (stack.length > 0) {\n for (const el of stack.pop()) {\n if (Array.isArray(el)) stack.push(el as ReadonlyDeepArray<T>);\n else result.add(el as T);\n }\n }\n return result;\n}\n"],"mappings":";;;;;;;AAQO,SAASA,QAAQA,CAAIC,OAAqB,EAAwB;EACvE,OAAOC,MAAM,CAACC,MAAM,CAACF,OAAO,CAAC;AAC/B;AAEO,SAASG,YAAYA,CAC1BC,GAAyB,EACjB;EACR,MAAMC,MAAM,GAAG,IAAIC,GAAG,CAAI,CAAC;EAC3B,MAAMC,KAAK,GAAG,CAACH,GAAG,CAAC;EACnB,OAAOG,KAAK,CAACC,MAAM,GAAG,CAAC,EAAE;IACvB,KAAK,MAAMC,EAAE,IAAIF,KAAK,CAACG,GAAG,CAAC,CAAC,EAAE;MAC5B,IAAIC,KAAK,CAACC,OAAO,CAACH,EAAE,CAAC,EAAEF,KAAK,CAACM,IAAI,CAACJ,EAA0B,CAAC,CAAC,KACzDJ,MAAM,CAACS,GAAG,CAACL,EAAO,CAAC;IAC1B;EACF;EACA,OAAOJ,MAAM;AACf;AAAC","ignoreList":[]}

View File

@@ -1,12 +0,0 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.getEnv = getEnv;
function getEnv(defaultValue = "development") {
return process.env.BABEL_ENV || process.env.NODE_ENV || defaultValue;
}
0 && 0;
//# sourceMappingURL=environment.js.map

View File

@@ -1 +0,0 @@
{"version":3,"names":["getEnv","defaultValue","process","env","BABEL_ENV","NODE_ENV"],"sources":["../../../src/config/helpers/environment.ts"],"sourcesContent":["export function getEnv(defaultValue: string = \"development\"): string {\n return process.env.BABEL_ENV || process.env.NODE_ENV || defaultValue;\n}\n"],"mappings":";;;;;;AAAO,SAASA,MAAMA,CAACC,YAAoB,GAAG,aAAa,EAAU;EACnE,OAAOC,OAAO,CAACC,GAAG,CAACC,SAAS,IAAIF,OAAO,CAACC,GAAG,CAACE,QAAQ,IAAIJ,YAAY;AACtE;AAAC","ignoreList":[]}

View File

@@ -1,93 +0,0 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.createConfigItem = createConfigItem;
exports.createConfigItemAsync = createConfigItemAsync;
exports.createConfigItemSync = createConfigItemSync;
Object.defineProperty(exports, "default", {
enumerable: true,
get: function () {
return _full.default;
}
});
exports.loadOptions = loadOptions;
exports.loadOptionsAsync = loadOptionsAsync;
exports.loadOptionsSync = loadOptionsSync;
exports.loadPartialConfig = loadPartialConfig;
exports.loadPartialConfigAsync = loadPartialConfigAsync;
exports.loadPartialConfigSync = loadPartialConfigSync;
function _gensync() {
const data = require("gensync");
_gensync = function () {
return data;
};
return data;
}
var _full = require("./full.js");
var _partial = require("./partial.js");
var _item = require("./item.js");
var _rewriteStackTrace = require("../errors/rewrite-stack-trace.js");
const loadPartialConfigRunner = _gensync()(_partial.loadPartialConfig);
function loadPartialConfigAsync(...args) {
return (0, _rewriteStackTrace.beginHiddenCallStack)(loadPartialConfigRunner.async)(...args);
}
function loadPartialConfigSync(...args) {
return (0, _rewriteStackTrace.beginHiddenCallStack)(loadPartialConfigRunner.sync)(...args);
}
function loadPartialConfig(opts, callback) {
if (callback !== undefined) {
(0, _rewriteStackTrace.beginHiddenCallStack)(loadPartialConfigRunner.errback)(opts, callback);
} else if (typeof opts === "function") {
(0, _rewriteStackTrace.beginHiddenCallStack)(loadPartialConfigRunner.errback)(undefined, opts);
} else {
{
return loadPartialConfigSync(opts);
}
}
}
function* loadOptionsImpl(opts) {
var _config$options;
const config = yield* (0, _full.default)(opts);
return (_config$options = config == null ? void 0 : config.options) != null ? _config$options : null;
}
const loadOptionsRunner = _gensync()(loadOptionsImpl);
function loadOptionsAsync(...args) {
return (0, _rewriteStackTrace.beginHiddenCallStack)(loadOptionsRunner.async)(...args);
}
function loadOptionsSync(...args) {
return (0, _rewriteStackTrace.beginHiddenCallStack)(loadOptionsRunner.sync)(...args);
}
function loadOptions(opts, callback) {
if (callback !== undefined) {
(0, _rewriteStackTrace.beginHiddenCallStack)(loadOptionsRunner.errback)(opts, callback);
} else if (typeof opts === "function") {
(0, _rewriteStackTrace.beginHiddenCallStack)(loadOptionsRunner.errback)(undefined, opts);
} else {
{
return loadOptionsSync(opts);
}
}
}
const createConfigItemRunner = _gensync()(_item.createConfigItem);
function createConfigItemAsync(...args) {
return (0, _rewriteStackTrace.beginHiddenCallStack)(createConfigItemRunner.async)(...args);
}
function createConfigItemSync(...args) {
return (0, _rewriteStackTrace.beginHiddenCallStack)(createConfigItemRunner.sync)(...args);
}
function createConfigItem(target, options, callback) {
if (callback !== undefined) {
(0, _rewriteStackTrace.beginHiddenCallStack)(createConfigItemRunner.errback)(target, options, callback);
} else if (typeof options === "function") {
(0, _rewriteStackTrace.beginHiddenCallStack)(createConfigItemRunner.errback)(target, undefined, callback);
} else {
{
return createConfigItemSync(target, options);
}
}
}
0 && 0;
//# sourceMappingURL=index.js.map

File diff suppressed because one or more lines are too long

View File

@@ -1,67 +0,0 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.createConfigItem = createConfigItem;
exports.createItemFromDescriptor = createItemFromDescriptor;
exports.getItemDescriptor = getItemDescriptor;
function _path() {
const data = require("path");
_path = function () {
return data;
};
return data;
}
var _configDescriptors = require("./config-descriptors.js");
function createItemFromDescriptor(desc) {
return new ConfigItem(desc);
}
function* createConfigItem(value, {
dirname = ".",
type
} = {}) {
const descriptor = yield* (0, _configDescriptors.createDescriptor)(value, _path().resolve(dirname), {
type,
alias: "programmatic item"
});
return createItemFromDescriptor(descriptor);
}
const CONFIG_ITEM_BRAND = Symbol.for("@babel/core@7 - ConfigItem");
function getItemDescriptor(item) {
if (item != null && item[CONFIG_ITEM_BRAND]) {
return item._descriptor;
}
return undefined;
}
class ConfigItem {
constructor(descriptor) {
this._descriptor = void 0;
this[CONFIG_ITEM_BRAND] = true;
this.value = void 0;
this.options = void 0;
this.dirname = void 0;
this.name = void 0;
this.file = void 0;
this._descriptor = descriptor;
Object.defineProperty(this, "_descriptor", {
enumerable: false
});
Object.defineProperty(this, CONFIG_ITEM_BRAND, {
enumerable: false
});
this.value = this._descriptor.value;
this.options = this._descriptor.options;
this.dirname = this._descriptor.dirname;
this.name = this._descriptor.name;
this.file = this._descriptor.file ? {
request: this._descriptor.file.request,
resolved: this._descriptor.file.resolved
} : undefined;
Object.freeze(this);
}
}
Object.freeze(ConfigItem.prototype);
0 && 0;
//# sourceMappingURL=item.js.map

File diff suppressed because one or more lines are too long

View File

@@ -1,158 +0,0 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = loadPrivatePartialConfig;
exports.loadPartialConfig = loadPartialConfig;
function _path() {
const data = require("path");
_path = function () {
return data;
};
return data;
}
var _plugin = require("./plugin.js");
var _util = require("./util.js");
var _item = require("./item.js");
var _configChain = require("./config-chain.js");
var _environment = require("./helpers/environment.js");
var _options = require("./validation/options.js");
var _index = require("./files/index.js");
var _resolveTargets = require("./resolve-targets.js");
const _excluded = ["showIgnoredFiles"];
function _objectWithoutPropertiesLoose(r, e) { if (null == r) return {}; var t = {}; for (var n in r) if ({}.hasOwnProperty.call(r, n)) { if (-1 !== e.indexOf(n)) continue; t[n] = r[n]; } return t; }
function resolveRootMode(rootDir, rootMode) {
switch (rootMode) {
case "root":
return rootDir;
case "upward-optional":
{
const upwardRootDir = (0, _index.findConfigUpwards)(rootDir);
return upwardRootDir === null ? rootDir : upwardRootDir;
}
case "upward":
{
const upwardRootDir = (0, _index.findConfigUpwards)(rootDir);
if (upwardRootDir !== null) return upwardRootDir;
throw Object.assign(new Error(`Babel was run with rootMode:"upward" but a root could not ` + `be found when searching upward from "${rootDir}".\n` + `One of the following config files must be in the directory tree: ` + `"${_index.ROOT_CONFIG_FILENAMES.join(", ")}".`), {
code: "BABEL_ROOT_NOT_FOUND",
dirname: rootDir
});
}
default:
throw new Error(`Assertion failure - unknown rootMode value.`);
}
}
function* loadPrivatePartialConfig(inputOpts) {
if (inputOpts != null && (typeof inputOpts !== "object" || Array.isArray(inputOpts))) {
throw new Error("Babel options must be an object, null, or undefined");
}
const args = inputOpts ? (0, _options.validate)("arguments", inputOpts) : {};
const {
envName = (0, _environment.getEnv)(),
cwd = ".",
root: rootDir = ".",
rootMode = "root",
caller,
cloneInputAst = true
} = args;
const absoluteCwd = _path().resolve(cwd);
const absoluteRootDir = resolveRootMode(_path().resolve(absoluteCwd, rootDir), rootMode);
const filename = typeof args.filename === "string" ? _path().resolve(cwd, args.filename) : undefined;
const showConfigPath = yield* (0, _index.resolveShowConfigPath)(absoluteCwd);
const context = {
filename,
cwd: absoluteCwd,
root: absoluteRootDir,
envName,
caller,
showConfig: showConfigPath === filename
};
const configChain = yield* (0, _configChain.buildRootChain)(args, context);
if (!configChain) return null;
const merged = {
assumptions: {}
};
configChain.options.forEach(opts => {
(0, _util.mergeOptions)(merged, opts);
});
const options = Object.assign({}, merged, {
targets: (0, _resolveTargets.resolveTargets)(merged, absoluteRootDir),
cloneInputAst,
babelrc: false,
configFile: false,
browserslistConfigFile: false,
passPerPreset: false,
envName: context.envName,
cwd: context.cwd,
root: context.root,
rootMode: "root",
filename: typeof context.filename === "string" ? context.filename : undefined,
plugins: configChain.plugins.map(descriptor => (0, _item.createItemFromDescriptor)(descriptor)),
presets: configChain.presets.map(descriptor => (0, _item.createItemFromDescriptor)(descriptor))
});
return {
options,
context,
fileHandling: configChain.fileHandling,
ignore: configChain.ignore,
babelrc: configChain.babelrc,
config: configChain.config,
files: configChain.files
};
}
function* loadPartialConfig(opts) {
let showIgnoredFiles = false;
if (typeof opts === "object" && opts !== null && !Array.isArray(opts)) {
var _opts = opts;
({
showIgnoredFiles
} = _opts);
opts = _objectWithoutPropertiesLoose(_opts, _excluded);
_opts;
}
const result = yield* loadPrivatePartialConfig(opts);
if (!result) return null;
const {
options,
babelrc,
ignore,
config,
fileHandling,
files
} = result;
if (fileHandling === "ignored" && !showIgnoredFiles) {
return null;
}
(options.plugins || []).forEach(item => {
if (item.value instanceof _plugin.default) {
throw new Error("Passing cached plugin instances is not supported in " + "babel.loadPartialConfig()");
}
});
return new PartialConfig(options, babelrc ? babelrc.filepath : undefined, ignore ? ignore.filepath : undefined, config ? config.filepath : undefined, fileHandling, files);
}
class PartialConfig {
constructor(options, babelrc, ignore, config, fileHandling, files) {
this.options = void 0;
this.babelrc = void 0;
this.babelignore = void 0;
this.config = void 0;
this.fileHandling = void 0;
this.files = void 0;
this.options = options;
this.babelignore = ignore;
this.babelrc = babelrc;
this.config = config;
this.fileHandling = fileHandling;
this.files = files;
Object.freeze(this);
}
hasFilesystemConfig() {
return this.babelrc !== undefined || this.config !== undefined;
}
}
Object.freeze(PartialConfig.prototype);
0 && 0;
//# sourceMappingURL=partial.js.map

Some files were not shown because too many files have changed in this diff Show More