Skip to content

v0.6.0 — Modes, drawing canvas, bookmarks, editor & command palette#6

Merged
devohmycode merged 2 commits intomasterfrom
0.6.0
Feb 25, 2026
Merged

v0.6.0 — Modes, drawing canvas, bookmarks, editor & command palette#6
devohmycode merged 2 commits intomasterfrom
0.6.0

Conversation

@devohmycode
Copy link
Copy Markdown
Owner

@devohmycode devohmycode commented Feb 25, 2026

Summary

  • 5 integrated modes with icon tab bar and Ctrl+15 shortcuts: SuperFlux (RSS), SuperBookmark, SuperNote, SuperEditor, SuperDraw
  • SuperDraw — custom canvas drawing tool with 10 tools (select, hand, rect, ellipse, diamond, line, arrow, freehand, text, eraser), color/fill picker, stroke width, font size selector, dark/light toggle, zoom/pan, undo/redo, selection with resize handles, PNG export
  • SuperEditor — document editor with folder organization and Pandoc export (PDF, DOCX, HTML, Markdown)
  • SuperBookmark — web bookmark manager with metadata extraction and built-in reader
  • Command palette (Ctrl+K) with search, keyboard shortcuts overlay (?)
  • Mode tab bar (icon-only) in the source panel with Ctrl+K badge
  • Markdown rendering in sticky notes (post-it board + card view) via react-markdown
  • Color palette system with theme picker
  • Supabase migrations for bookmarks, editor documents, and notes
  • README updated with all new features, modes, shortcuts, and project structure

Changed files (41 files, +22k / -6k lines)

  • src/App.tsx — mode switching, brand transitions, panel routing
  • src/components/SuperDraw.tsx — new canvas drawing component
  • src/components/SuperEditor.tsx — new document editor
  • src/components/BookmarkPanel.tsx / BookmarkReader.tsx — bookmark management
  • src/components/CommandPalette.tsx / ShortcutsOverlay.tsx — palette & help
  • src/components/SourcePanel.tsx — mode tab bar, brand switch prop
  • src/components/NoteStickyBoard.tsx / NotePanel.tsx — Markdown rendering
  • src/services/ — bookmarkService, noteService, editorDocService, pandocService
  • src/index.css — mode tabs, sticky markdown, SuperDraw styles
  • supabase/migrations/ — 3 new migration files

Test plan

  • Switch between all 5 modes via icon tabs and Ctrl+15
  • Draw shapes, freehand, text, arrows in SuperDraw; test dark/light toggle
  • Create/edit/export documents in SuperEditor
  • Add/read bookmarks in SuperBookmark
  • Write Markdown in sticky notes and verify rendering
  • Open command palette with Ctrl+K, test search and shortcuts
  • Verify undo/redo, selection, resize, PNG export in SuperDraw

🤖 Generated with Claude Code

Summary by CodeRabbit

Release Notes

  • New Features

    • Rich-text document editor with export to Word/PDF formats
    • Drawing canvas with vector tools and sketching capabilities
    • Bookmarking system with read status tracking
    • Command palette for quick navigation (Ctrl+K)
    • Keyboard shortcuts reference overlay
    • 5 integrated productivity modes: RSS, Notes, Bookmarks, Editor, Draw
    • AMOLED theme and customizable color palettes
    • Resizable sticky notes with Markdown rendering
    • Feed drag-and-drop reordering
  • Documentation

    • Added comprehensive project overview and architecture guide

Introduce rich-editor and bookmark features plus theme/palette support. Adds many new UI components (SuperEditor, BookmarkPanel, BookmarkReader, EditorFileList, PalettePicker, GradientText, ShinyText, SpotlightCard, etc.), services for bookmarks and editor documents, and new Supabase migrations for bookmarks and editor_documents. Update index.html to handle an 'amoled' theme and persist selected palette, and adjust App/Auth/feeds/ui helpers to integrate these features. package.json updated to include editor-related dependencies (e.g. Tiptap, Headless UI) and other supporting libraries.
Introduce five integrated app modes (Flux, Bookmark, Note, Editor, Draw) and wire up a command palette + shortcuts overlay. Adds new UI components (CommandPalette, ShortcutsOverlay, SuperDraw, updates to SuperEditor, NotePanel, SourcePanel and related components), a useCommands hook for keyboard/command registration, and services for notes and Pandoc export. Includes a Supabase migration to support notes, updates to App.tsx and the Tauri backend, README/AGENTS documentation updates, and package.json dependency changes to support new features.
@qodo-code-review
Copy link
Copy Markdown

Review Summary by Qodo

v0.6.0 — Multi-mode integration with SuperDraw, SuperEditor, SuperBookmark, command palette, and color themes

✨ Enhancement

Grey Divider

Walkthroughs

Description
• **5 integrated modes** with icon tab bar and Ctrl+15 shortcuts: SuperFlux (RSS),
  SuperBookmark, SuperNote, SuperEditor, SuperDraw
• **SuperDraw** — custom canvas drawing tool with 10 tools (select, hand, rect, ellipse, diamond,
  line, arrow, freehand, text, eraser), color/fill picker, stroke width, font size selector,
  dark/light toggle, zoom/pan, undo/redo, selection with resize handles, PNG export
• **SuperEditor** — rich text document editor with TipTap, folder organization, Pandoc export (PDF,
  DOCX, HTML), emoji suggestions, and task lists
• **SuperBookmark** — web bookmark manager with metadata extraction, reader mode with article
  extraction, and dual view modes (cards/list)
• **Command palette** (Ctrl+K) with fuzzy search, keyboard shortcuts overlay (?), and 20+
  registered commands
• **Mode tab bar** (icon-only) in the source panel with mode switching and palette picker
  integration
• **Markdown rendering** in sticky notes (post-it board + card view) with resizable sticky notes via
  react-markdown
• **Color palette system** with 10 customizable themes (Amber, Ocean, Forest, Sunset, Lavender,
  Rosewood, Mint, Neon, Slate) and AMOLED dark theme
• **Supabase migrations** for bookmarks, editor documents, and notes with RLS policies and optimized
  indexes
• **Tauri Pandoc integration** for document import/export with base64 encoding and temporary file
  handling
• **Remote deletion detection** in sync service to prevent orphaned feed/item re-syncing
• **Feed reordering** via drag-and-drop with programmatic panel width control
• **New UI components**: ShinyText, GradientText, SpotlightCard, PalettePicker, EditorFileList,
  BookmarkReader, CommandPalette, ShortcutsOverlay
• **Documentation** updated with feature overview, keyboard shortcuts, architecture patterns, and
  agent guidance
Diagram
flowchart LR
  A["App.tsx<br/>Mode Switching"] -->|Ctrl+1-5| B["5 Modes"]
  B --> C["SuperFlux<br/>RSS Reader"]
  B --> D["SuperBookmark<br/>Web Manager"]
  B --> E["SuperNote<br/>Sticky Board"]
  B --> F["SuperEditor<br/>Rich Text"]
  B --> G["SuperDraw<br/>Canvas Tool"]
  A -->|Ctrl+K| H["Command Palette<br/>Fuzzy Search"]
  A -->|?| I["Shortcuts Overlay"]
  J["SourcePanel"] -->|Mode Tabs| A
  J -->|Palette Picker| K["10 Color Themes"]
  F -->|Pandoc| L["Export DOCX/PDF"]
  D -->|Article Extract| M["Bookmark Reader"]
  E -->|Markdown| N["Sticky Notes"]
  O["Supabase"] -->|Sync| P["Bookmarks<br/>Documents<br/>Notes"]
  Q["syncService"] -->|Orphan Detection| O
Loading

Grey Divider

File Changes

1. src/themes/palettes.ts ✨ Enhancement +262/-0

Color palette system with 10 customizable themes

• New color palette system with 10 themes (Amber, Ocean, Forest, Sunset, Lavender, Rosewood, Mint,
 Neon, Slate)
• Each palette defines light/dark mode colors for accent, secondary, tertiary with glow variants
• Utility functions for storing, retrieving, and applying palettes via data-palette attribute
• localStorage persistence with fallback to 'amber' theme

src/themes/palettes.ts


2. src/services/syncService.ts 🐞 Bug fix +34/-6

Remote deletion detection and orphan feed/item handling

• Track synced feed and item IDs in localStorage for orphan detection
• Detect and skip remote deletions (feeds/items previously synced but missing from Supabase)
• Filter out deleted feeds and items during merge to prevent re-syncing orphaned data
• Store synced IDs after push operations for future deletion tracking

src/services/syncService.ts


3. src/services/noteService.ts ✨ Enhancement +151/-0

Note service with Supabase sync and sticky positioning

• New service for Supabase note operations (fetch, upsert, remove, update content/metadata)
• Support for sticky note positioning (x, y, rotation, z-index, color, dimensions)
• Folder organization for notes with metadata updates
• Error handling with console logging for debugging

src/services/noteService.ts


View more (37)
4. src/hooks/useCommands.ts ✨ Enhancement +117/-0

Command palette and keyboard shortcut hook

• Custom hook for command palette and keyboard shortcut management
• Parse shortcut strings into keybind objects with modifier support
• Command registration with conditional execution via when callbacks
• Palette toggle (Ctrl+K) and help overlay (?) with input detection

src/hooks/useCommands.ts


5. src/services/bookmarkService.ts ✨ Enhancement +103/-0

Web bookmark service with metadata extraction

• New service for web bookmark management (fetch, add, remove, toggle read status)
• Bookmark metadata: URL, title, excerpt, image, favicon, author, site_name, tags, note
• Deterministic bookmark ID generation via URL hash for sync compatibility
• Supabase integration with user isolation

src/services/bookmarkService.ts


6. src/services/editorDocService.ts ✨ Enhancement +112/-0

Editor document service with folder organization

• New service for editor document CRUD operations (fetch, upsert, remove, update)
• Support for document organization via folders
• Separate content and metadata update functions with error handling
• Supabase persistence with user isolation

src/services/editorDocService.ts


7. src/services/pandocService.ts ✨ Enhancement +76/-0

Pandoc document conversion service for desktop

• Pandoc integration for document import/export (DOCX, PDF, HTML)
• Tauri-based command invocation with base64 encoding for file transfer
• Web fallback using mammoth library for DOCX import only
• Temporary file handling with cleanup after conversion

src/services/pandocService.ts


8. src/hooks/useFeedStore.ts ✨ Enhancement +18/-0

Feed reordering functionality

• New reorderFeed function to move feeds before/after other feeds in the array
• Prevents reordering a feed to itself
• Maintains feed list integrity during reorder operations

src/hooks/useFeedStore.ts


9. src/hooks/useResizablePanels.ts ✨ Enhancement +5/-1

Programmatic panel width control

• Export setWidthsOverride callback to allow programmatic panel width adjustment
• Enable dynamic panel resizing when switching between modes

src/hooks/useResizablePanels.ts


10. src/services/rssService.ts 🐞 Bug fix +3/-0

Skip invalid feed URLs during fetch

• Skip feeds with empty or missing URLs to prevent errors on virtual feeds
• Prevents attempting to fetch from invalid feed sources

src/services/rssService.ts


11. src-tauri/src/lib.rs ✨ Enhancement +88/-1

Pandoc Tauri command integration

• Three new Tauri commands: pandoc_check, pandoc_import, pandoc_exportpandoc_check verifies pandoc availability and returns version
• pandoc_import converts DOCX/PDF to HTML via pandoc with base64 encoding
• pandoc_export converts HTML to DOCX/PDF with temporary file cleanup
• Register new commands in Tauri handler

src-tauri/src/lib.rs


12. index.html ✨ Enhancement +5/-1

AMOLED theme and palette initialization

• Add AMOLED dark theme detection and application
• Initialize palette from localStorage on page load via data-palette attribute
• Preserve theme preference across sessions

index.html


13. src/components/SuperDraw.tsx ✨ Enhancement +585/-0

SuperDraw canvas drawing tool with 10 tools

• New canvas drawing component with 10 tools (select, hand, rect, ellipse, diamond, line, arrow,
 freehand, text, eraser)
• Full drawing features: color/fill picker, stroke width, font size, zoom/pan, undo/redo, selection
 with resize handles
• Dark/light mode toggle with adaptive colors
• PNG export and localStorage persistence
• Keyboard shortcuts for all tools and operations

src/components/SuperDraw.tsx


14. src/App.tsx ✨ Enhancement +422/-39

Multi-mode integration with command palette and search

• Integrate 5 modes: SuperFlux (RSS), SuperBookmark, SuperNote, SuperEditor, SuperDraw
• Add mode switching with Ctrl+1–5 shortcuts and brand transition animation
• Implement command palette (Ctrl+K) and shortcuts overlay (?)
• Add search functionality for notes and feed items
• Sync editor docs and notes from Supabase on login with local fallback
• Dynamic panel width adjustment based on active mode
• Register 20+ commands for navigation, actions, modes, and feeds

src/App.tsx


15. src/components/FeedPanel.tsx ✨ Enhancement +23/-9

Feed panel pagination and spotlight card styling

• Increase items per page: 10 (normal) / 20 (compact) for better pagination
• Wrap feed cards in SpotlightCard component for visual enhancement
• Add gradient text styling to feed panel title
• Adjust pagination info display to use dynamic itemsPerPage

src/components/FeedPanel.tsx


16. package.json Dependencies +24/-1

Dependencies for editor, markdown, and UI enhancements

• Add Tiptap editor dependencies (starter-kit, extensions for formatting, links, images, tasks)
• Add markdown rendering (react-markdown)
• Add document conversion (mammoth for DOCX)
• Add UI components (@headlessui/react, tippy.js)
• Add syntax highlighting (lowlight)

package.json


17. src/index.css ✨ Enhancement +6043/-3620

AMOLED theme, color palettes, and new component styles

• Replaced sepia theme with new amoled theme featuring pure black backgrounds (#000000) and
 optimized colors for OLED displays
• Added 8 new color palettes (Ocean, Forest, Sunset, Lavender, Rosewood, Mint, Neon, Slate) with
 light/dark variants using [data-palette="..."] selectors
• Introduced secondary and tertiary accent color variables (--accent-secondary,
 --accent-tertiary) across all themes for enhanced color hierarchy
• Added comprehensive styling for new UI components: mode tab bar, SuperBookmark panel with
 card/list views, bookmark reader with article rendering, SuperEditor with rich text toolbar,
 SuperDraw canvas, and Command Palette
• Implemented Markdown rendering styles for sticky notes and note cards with support for headings,
 lists, code blocks, blockquotes, and links

src/index.css


18. src/components/SuperEditor.tsx ✨ Enhancement +483/-0

Rich text editor with Pandoc export and emoji support

• New rich text editor component using TipTap with 20+ formatting options (bold, italic, underline,
 headings, lists, code blocks, etc.)
• Integrated Pandoc-based import/export for DOCX and PDF formats with error handling
• File menu with keyboard shortcuts (Ctrl+N, Ctrl+O, Ctrl+S) for document management
• Emoji suggestion system with GitHub emoji support and keyboard navigation
• Character and word count display in footer; drag handle for block manipulation
• Supports task lists, syntax-highlighted code blocks, links, images, and text alignment

src/components/SuperEditor.tsx


19. src/components/ui/animated-theme-toggler.tsx ✨ Enhancement +11/-8

Theme toggler updated for AMOLED support

• Replaced sepia theme with amoled theme in the theme cycle (light → dark → amoled)
• Updated theme icon from Sunrise to Eclipse for AMOLED mode
• Simplified theme application logic to handle three distinct themes instead of four
• Added tooltip titles for theme toggle button showing current theme name

src/components/ui/animated-theme-toggler.tsx


20. src/components/SourcePanel.tsx ✨ Enhancement +240/-14

Multi-mode support with editor, bookmarks, palette picker

• Extended brandMode prop to support 5 modes (flux, bookmark, note, editor, draw) with
 new onBrandSwitch callback
• Added mode tab bar with icon buttons and Ctrl+K hint for command palette
• Integrated EditorFileList component for editor mode with document/folder management
• Implemented bookmark URL input bar with form submission and keyboard handling
• Added feed reordering via drag-and-drop with dropFeedTarget state tracking before/after
 positioning
• Integrated PalettePicker component in footer for color palette selection
• Added search query filtering for feeds and folders with searchQuery prop
• Enhanced footer button logic to support different actions per mode (add flux, note, document, or
 bookmark)

src/components/SourcePanel.tsx


21. src/components/EditorFileList.tsx ✨ Enhancement +454/-0

New editor file list component with folder management

• New component for managing editor documents with folder organization
• Supports document CRUD (create, rename, delete) and folder operations
• Context menus for documents and folders with move-to-folder submenu
• Inline rename inputs with keyboard shortcuts (Enter to save, Escape to cancel)
• Folder expansion/collapse with animated transitions
• Displays all documents and folder-scoped document counts
• Exports EditorDoc interface and localStorage persistence helpers

src/components/EditorFileList.tsx


22. src/components/BookmarkPanel.tsx ✨ Enhancement +293/-0

New bookmark panel with cards and compact list views

• New component for displaying and managing web bookmarks
• Two view modes: compact list and animated card grid with gradient blobs
• Filter bookmarks by read/unread status
• Displays metadata (favicon, site name, date, source, author)
• Mark as read/unread and delete actions with event propagation control
• Animated card entrance with staggered delays and hover scale effects
• Gradient animations per bookmark source (chrome, desktop, mobile)

src/components/BookmarkPanel.tsx


23. src/components/NoteStickyBoard.tsx ✨ Enhancement +78/-13

Markdown rendering and resizable sticky notes

• Added react-markdown rendering for note content instead of plain text
• Implemented resizable sticky notes with min/max width/height constraints
• Added resize handle with pointer events and visual resize indicator
• Stores stickyWidth and stickyHeight properties on notes
• Markdown content displays in read mode; double-click to edit
• Placeholder text shown for empty notes

src/components/NoteStickyBoard.tsx


24. src/components/BookmarkReader.tsx ✨ Enhancement +249/-0

New bookmark reader with article extraction and web view

• New component for reading extracted bookmark content with reader and web modes
• Reader mode extracts article content via extractArticle() service with loading/error states
• Web mode displays bookmark URL in iframe with sandbox restrictions
• Font size controls (A−/A+) for reader mode
• Toolbar with favicon, site name, view toggle, and external link button
• Auto-marks bookmark as read when content loads
• Retry mechanism for failed article extractions

src/components/BookmarkReader.tsx


25. README.md 📝 Documentation +74/-11

Documentation for 5 modes and new features

• Updated project description to include bookmarks, notes, document editor, and drawing canvas
• Added new "5 Integrated Modes" section with table of modes and shortcuts (Ctrl+15)
• Documented SuperBookmark, SuperNote, SuperEditor, and SuperDraw features
• Updated keyboard shortcuts table with new mode switching and command palette shortcuts
• Added theme variants (AMOLED) to appearance section
• Updated file structure to include new components and services
• Expanded architecture section with mode switching and new service descriptions

README.md


26. AGENTS.md 📝 Documentation +104/-0

New agent guidance document for codebase navigation

• New file providing guidance for WARP and AI agents working with the codebase
• Documents project overview, development commands, and architecture patterns
• Explains dual runtime abstraction (Tauri vs browser), state management, and sync layers
• Describes Rust backend commands and Supabase schema
• Lists environment variables and coding conventions (French UI text, Tailwind CSS, Framer Motion)

AGENTS.md


27. src/components/CommandPalette.tsx ✨ Enhancement +148/-0

New command palette with fuzzy search and navigation

• New command palette component triggered by Ctrl+K
• Fuzzy search filtering with category grouping
• Keyboard navigation (arrow keys, Ctrl+J/K, Enter to execute)
• Auto-focus input on open, scroll selected item into view
• Displays commands with optional keyboard shortcuts
• Closes on Escape or backdrop click

src/components/CommandPalette.tsx


28. src/components/ShinyText.tsx ✨ Enhancement +123/-0

New shiny text animation component

• New animated text component with horizontal shine effect
• Configurable animation speed, direction (left/right), spread angle, and color
• Supports yoyo (back-and-forth) animation mode
• Pause on hover option with motion value tracking
• Uses Framer Motion for smooth gradient animation

src/components/ShinyText.tsx


29. src/components/GradientText.tsx ✨ Enhancement +130/-0

New gradient text animation component

• New animated gradient text component with multiple animation directions
• Supports horizontal, vertical, and diagonal gradient flows
• Configurable colors array, animation speed, and yoyo mode
• Pause on hover functionality
• Optional border display with gradient background
• Uses Framer Motion for smooth color transitions

src/components/GradientText.tsx


30. src/components/NotePanel.tsx ✨ Enhancement +21/-8

Markdown rendering and gradient title animation

• Added GradientText component to title with animated gradient effect
• Integrated react-markdown for rendering note content in card view
• Changed default view mode from cards to board
• Removed add note button from header (moved to footer in SourcePanel)
• Added stickyWidth and stickyHeight to Note interface
• Markdown content displays in cards with fallback for empty notes

src/components/NotePanel.tsx


31. src/components/PalettePicker.tsx ✨ Enhancement +95/-0

New palette picker component for theme colors

• New color palette picker component with dropdown and inline variants
• Displays palette options with color dot previews
• Applies selected palette via applyPalette() function
• Closes on click outside, Escape key, or selection
• Shows checkmark for currently active palette
• Inline version for use in settings modal

src/components/PalettePicker.tsx


32. src/components/SettingsModal.tsx ✨ Enhancement +8/-4

Palette picker integration and AMOLED theme support

• Added PalettePickerInline component to appearance settings
• Updated window effect color detection to include AMOLED theme (pure black)
• AMOLED theme now uses RGB(0,0,0) instead of dark gray for window effects

src/components/SettingsModal.tsx


33. supabase/migrations/20250225000007_notes.sql Configuration +52/-0

Database migration for notes with sticky board properties

• New migration creating notes table with sticky board columns
• Columns: id, user_id, title, content, folder, timestamps
• Sticky board properties: sticky_x, sticky_y, sticky_rotation, sticky_z_index,
 sticky_color, sticky_width, sticky_height
• Indexes on user/updated_at and user/folder for query optimization
• RLS policies for user-scoped access (select, insert, update, delete)
• Auto-update trigger for updated_at timestamp

supabase/migrations/20250225000007_notes.sql


34. src/components/SpotlightCard.tsx ✨ Enhancement +74/-0

New spotlight card effect component

• New spotlight effect component that tracks mouse position
• Renders radial gradient spotlight following cursor movement
• Opacity controlled by focus/blur and mouse enter/leave events
• Configurable spotlight color with default semi-transparent white
• Used for interactive card hover effects

src/components/SpotlightCard.tsx


35. src/components/ShortcutsOverlay.tsx ✨ Enhancement +57/-0

New keyboard shortcuts help overlay

• New keyboard shortcuts help overlay triggered by ? key
• Displays all commands with shortcuts grouped by category
• Closes on Escape or ? key press
• Renders as modal backdrop with grid layout of shortcut rows
• Shows label and keyboard notation for each shortcut

src/components/ShortcutsOverlay.tsx


36. supabase/migrations/20250225000005_bookmarks.sql Configuration +40/-0

Database migration for web bookmarks

• New migration creating bookmarks table for web bookmark storage
• Columns: id, user_id, url, title, excerpt, image, favicon, author, site_name,
 tags, note, is_read, source
• Source field restricted to chrome, desktop, mobile via check constraint
• Indexes on user/updated_at and user/url for efficient queries
• RLS policies for user-scoped access (select, insert, update, delete)
• Auto-update trigger for updated_at timestamp

supabase/migrations/20250225000005_bookmarks.sql


37. src/main.tsx ✨ Enhancement +4/-4

AMOLED theme support for window effects

• Updated window effect color detection to support AMOLED theme
• AMOLED theme now uses pure black RGB(0,0,0) instead of dark gray
• Checks for amoled class before dark class in theme detection

src/main.tsx


38. supabase/migrations/20250225000006_editor_documents.sql Configuration +32/-0

Database migration for editor documents

• New migration creating editor_documents table for document storage
• Columns: id, user_id, title, content, folder, timestamps
• Indexes on user/updated_at and user/folder for query optimization
• RLS policies for user-scoped access (select, insert, update, delete)
• Auto-update trigger for updated_at timestamp

supabase/migrations/20250225000006_editor_documents.sql


39. src/contexts/AuthContext.tsx ✨ Enhancement +2/-0

Editor data cleanup on logout

• Added superflux_editor_docs and superflux_editor_folders to localStorage cleanup on logout
• Ensures editor data is cleared when user signs out

src/contexts/AuthContext.tsx


40. pnpm-lock.yaml Additional files +6727/-0

...

pnpm-lock.yaml


Grey Divider

Qodo Logo

@qodo-code-review
Copy link
Copy Markdown

qodo-code-review bot commented Feb 25, 2026

Code Review by Qodo

🐞 Bugs (5) 📘 Rule violations (5) 📎 Requirement gaps (0)

Grey Divider


Action required

1. Unsanitized article.content HTML 📘 Rule violation ⛨ Security
Description
article.content from external pages is rendered via dangerouslySetInnerHTML without
sanitization, enabling XSS if the extracted content contains scripts or malicious markup. This
violates the requirement to validate/sanitize external inputs before rendering.
Code

src/components/BookmarkReader.tsx[R216-219]

+                <div
+                  className="bk-reader-body"
+                  dangerouslySetInnerHTML={{ __html: article.content }}
+                />
Evidence
Rule 6 requires validation/sanitization of external inputs; the PR introduces direct HTML injection
of externally-extracted article content into the DOM via dangerouslySetInnerHTML.

Rule 6: Generic: Security-First Input Validation and Data Handling
src/components/BookmarkReader.tsx[216-219]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`article.content` (external, untrusted HTML) is injected into the DOM with `dangerouslySetInnerHTML` without sanitization, creating an XSS risk.

## Issue Context
`extractArticle(bookmark.url)` fetches/parses content from arbitrary websites, so `article.content` must be treated as untrusted.

## Fix Focus Areas
- src/components/BookmarkReader.tsx[216-219]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. importError exposes raw error 📘 Rule violation ⛨ Security
Description
The UI displays err.message directly to users via the import/export toast, which can leak internal
details (e.g., stack/context, file paths, backend error text). User-facing errors should be generic
while detailed errors go only to internal logs.
Code

src/components/SuperEditor.tsx[R344-473]

+    } catch (err: any) {
+      setImportError(err?.message || 'Erreur lors de l\'import');
+    }
+    e.target.value = '';
+  }, [editor, doc, onUpdateContent]);
+
+  const handleExport = useCallback(async (format: 'docx' | 'pdf') => {
+    if (!editor) return;
+    setImportError(null);
+    try {
+      const html = editor.getHTML();
+      const blob = await exportWithPandoc(html, format);
+      const title = doc?.title || 'supereditor-document';
+      const url = URL.createObjectURL(blob);
+      const a = document.createElement('a');
+      a.href = url;
+      a.download = `${title.replace(/[^a-zA-Z0-9-_ ]/g, '')}.${format}`;
+      a.click();
+      URL.revokeObjectURL(url);
+    } catch (err: any) {
+      setImportError(err?.message || 'Erreur lors de l\'export');
+    }
+  }, [editor, doc?.title]);
+
+  // Keyboard shortcuts for file menu
+  useEffect(() => {
+    const handleKeyDown = (e: KeyboardEvent) => {
+      if (!editor) return;
+      if (e.ctrlKey && e.key === 'n') { e.preventDefault(); handleNew(); }
+      if (e.ctrlKey && e.key === 'o') { e.preventDefault(); handleOpen(); }
+      if (e.ctrlKey && e.key === 's') { e.preventDefault(); handleDownload(); }
+    };
+    window.addEventListener('keydown', handleKeyDown);
+    return () => window.removeEventListener('keydown', handleKeyDown);
+  }, [editor, handleNew, handleOpen, handleDownload]);
+
+  if (!editor) return null;
+
+  const chars = editor.storage.characterCount?.characters?.() ?? 0;
+  const words = editor.storage.characterCount?.words?.() ?? 0;
+
+  return (
+    <div className="super-editor-root">
+      <input
+        ref={fileInputRef}
+        type="file"
+        accept=".html,.htm,.txt,.md"
+        style={{ display: 'none' }}
+        onChange={handleFileChange}
+      />
+      <input
+        ref={importInputRef}
+        type="file"
+        accept=".docx,.pdf"
+        style={{ display: 'none' }}
+        onChange={handleImportChange}
+      />
+      {/* Toolbar */}
+      <div className="super-editor-toolbar">
+        <div className="super-editor-toolbar-row">
+          <FileMenu onNew={handleNew} onOpen={handleOpen} onDownload={handleDownload} onImport={handleImport} onExport={handleExport} />
+          <ToolSep />
+          <ToolBtn icon={Bold} label="Gras" active={editor.isActive('bold')} onClick={() => editor.chain().focus().toggleBold().run()} />
+          <ToolBtn icon={Italic} label="Italique" active={editor.isActive('italic')} onClick={() => editor.chain().focus().toggleItalic().run()} />
+          <ToolBtn icon={UnderlineIcon} label="Souligné" active={editor.isActive('underline')} onClick={() => editor.chain().focus().toggleUnderline().run()} />
+          <ToolBtn icon={Strikethrough} label="Barré" active={editor.isActive('strike')} onClick={() => editor.chain().focus().toggleStrike().run()} />
+          <ToolBtn icon={Code} label="Code" active={editor.isActive('code')} onClick={() => editor.chain().focus().toggleCode().run()} />
+
+          <ToolSep />
+
+          <ToolBtn icon={Heading1} label="H1" active={editor.isActive('heading', { level: 1 })} onClick={() => editor.chain().focus().toggleHeading({ level: 1 }).run()} />
+          <ToolBtn icon={Heading2} label="H2" active={editor.isActive('heading', { level: 2 })} onClick={() => editor.chain().focus().toggleHeading({ level: 2 }).run()} />
+          <ToolBtn icon={Heading3} label="H3" active={editor.isActive('heading', { level: 3 })} onClick={() => editor.chain().focus().toggleHeading({ level: 3 }).run()} />
+          <ToolBtn icon={Heading4} label="H4" active={editor.isActive('heading', { level: 4 })} onClick={() => editor.chain().focus().toggleHeading({ level: 4 }).run()} />
+
+          <ToolSep />
+
+          <ToolBtn icon={AlignLeft} label="Gauche" active={editor.isActive({ textAlign: 'left' })} onClick={() => editor.chain().focus().setTextAlign('left').run()} />
+          <ToolBtn icon={AlignCenter} label="Centre" active={editor.isActive({ textAlign: 'center' })} onClick={() => editor.chain().focus().setTextAlign('center').run()} />
+          <ToolBtn icon={AlignRight} label="Droite" active={editor.isActive({ textAlign: 'right' })} onClick={() => editor.chain().focus().setTextAlign('right').run()} />
+          <ToolBtn icon={AlignJustify} label="Justifié" active={editor.isActive({ textAlign: 'justify' })} onClick={() => editor.chain().focus().setTextAlign('justify').run()} />
+
+          <ToolSep />
+
+          <ToolBtn icon={List} label="Liste à puces" active={editor.isActive('bulletList')} onClick={() => editor.chain().focus().toggleBulletList().run()} />
+          <ToolBtn icon={ListOrdered} label="Liste numérotée" active={editor.isActive('orderedList')} onClick={() => editor.chain().focus().toggleOrderedList().run()} />
+          <ToolBtn icon={ListTodo} label="Liste de tâches" active={editor.isActive('taskList')} onClick={() => editor.chain().focus().toggleTaskList().run()} />
+          <ToolBtn icon={Quote} label="Citation" active={editor.isActive('blockquote')} onClick={() => editor.chain().focus().toggleBlockquote().run()} />
+          <ToolBtn icon={FileCode} label="Bloc de code" active={editor.isActive('codeBlock')} onClick={() => editor.chain().focus().toggleCodeBlock().run()} />
+          <ToolBtn icon={Minus} label="Séparateur" onClick={() => editor.chain().focus().setHorizontalRule().run()} />
+
+          <ToolSep />
+
+          <ToolBtn icon={LinkIcon} label="Lien" active={editor.isActive('link')} onClick={() => {
+            const url = window.prompt('URL du lien');
+            if (url) editor.chain().focus().extendMarkRange('link').setLink({ href: url }).run();
+          }} />
+          <ToolBtn icon={Unlink} label="Supprimer lien" onClick={() => editor.chain().focus().unsetLink().run()} />
+          <ToolBtn icon={ImageIcon} label="Image" onClick={() => {
+            const url = window.prompt('URL de l\'image');
+            if (url) editor.chain().focus().setImage({ src: url }).run();
+          }} />
+
+          <ToolSep />
+
+          <ToolBtn icon={SubscriptIcon} label="Indice" active={editor.isActive('subscript')} onClick={() => editor.chain().focus().toggleSubscript().run()} />
+          <ToolBtn icon={SuperscriptIcon} label="Exposant" active={editor.isActive('superscript')} onClick={() => editor.chain().focus().toggleSuperscript().run()} />
+
+          <ToolSep />
+
+          <ToolBtn icon={Undo2} label="Annuler" disabled={!editor.can().undo()} onClick={() => editor.chain().focus().undo().run()} />
+          <ToolBtn icon={Redo2} label="Rétablir" disabled={!editor.can().redo()} onClick={() => editor.chain().focus().redo().run()} />
+        </div>
+      </div>
+
+      {/* Editor content with drag handle */}
+      <div className="super-editor-content-wrapper">
+        <DragHandle editor={editor}>
+          <div className="super-editor-drag-handle">
+            <GripVertical size={14} />
+          </div>
+        </DragHandle>
+        <EditorContent editor={editor} className="super-editor-content" />
+      </div>
+
+      {/* Import/Export error toast */}
+      {importError && (
+        <div className="super-editor-toast" onClick={() => setImportError(null)}>
+          <span>⚠ {importError}</span>
+        </div>
Evidence
Rule 4 forbids exposing internal implementation details to end users; the PR stores err?.message
in state and renders it verbatim in the toast.

Rule 4: Generic: Secure Error Handling
src/components/SuperEditor.tsx[344-346]
src/components/SuperEditor.tsx[469-473]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The SuperEditor import/export flow surfaces `err.message` directly to users, potentially leaking internal details.

## Issue Context
Compliance requires generic user-facing errors; detailed diagnostics should go to internal logs only.

## Fix Focus Areas
- src/components/SuperEditor.tsx[344-346]
- src/components/SuperEditor.tsx[469-473]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. fetchNotes logs userId 📘 Rule violation ⛨ Security
Description
New console logs include userId and full error objects, which can expose sensitive identifiers and
potentially sensitive backend details in logs. Logs are also unstructured, reducing audit/debug
usefulness and violating secure logging requirements.
Code

src/services/noteService.ts[R19-36]

+export async function fetchNotes(userId: string): Promise<NoteRow[]> {
+  console.log('[notes] isSupabaseConfigured:', isSupabaseConfigured, 'userId:', userId);
+  if (!isSupabaseConfigured) { console.warn('[notes] Supabase not configured'); return []; }
+
+  const { data, error, status } = await supabase
+    .from('notes')
+    .select('*')
+    .eq('user_id', userId)
+    .order('updated_at', { ascending: false });
+
+  console.log('[notes] fetch response — status:', status, 'data:', data?.length, 'error:', error);
+
+  if (error) {
+    console.error('[notes] fetch error:', error);
+    return [];
+  }
+
+  return data ?? [];
Evidence
Rule 5 requires no sensitive data in logs and structured logging; the PR adds console logging of
userId and error objects in fetchNotes().

Rule 5: Generic: Secure Logging Practices
src/services/noteService.ts[19-33]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`fetchNotes()` logs sensitive identifiers (`userId`) and raw error objects to the console, and the log format is unstructured.

## Issue Context
Compliance requires logs to avoid PII/sensitive data and be structured for auditing/monitoring.

## Fix Focus Areas
- src/services/noteService.ts[19-33]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View more (1)
4. Pandoc path traversal 🐞 Bug ⛨ Security
Description
pandoc_import writes a temp file using tmp_dir.join(filename) where filename is provided by the
frontend. Absolute paths or '..' segments can escape the temp directory, enabling arbitrary file
overwrite on the user’s machine.
Code

src-tauri/src/lib.rs[R646-656]

+fn pandoc_import(base64_data: String, filename: String) -> Result<String, String> {
+    let bytes = STANDARD.decode(&base64_data)
+        .map_err(|e| format!("base64 decode error: {e}"))?;
+
+    let tmp_dir = std::env::temp_dir().join("superflux_pandoc");
+    std::fs::create_dir_all(&tmp_dir)
+        .map_err(|e| format!("Failed to create temp dir: {e}"))?;
+
+    let input_path = tmp_dir.join(&filename);
+    std::fs::write(&input_path, &bytes)
+        .map_err(|e| format!("Failed to write temp file: {e}"))?;
Evidence
The Rust command joins a user-controlled filename into a temp directory path and writes bytes there.
The frontend passes File.name as filename, which can contain path separators or absolute paths
depending on platform/source.

src-tauri/src/lib.rs[645-656]
src/services/pandocService.ts[15-26]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`pandoc_import` uses a user-provided `filename` to build a filesystem path (`tmp_dir.join(&amp;filename)`), allowing path traversal / absolute-path override.

### Issue Context
Frontend passes `file.name` to the backend.

### Fix Focus Areas
- src-tauri/src/lib.rs[645-674]
- src/services/pandocService.ts[15-26]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

5. extractArticle catch drops error 📘 Rule violation ⛯ Reliability
Description
The extractArticle() failure is caught without capturing/logging the error, losing crucial context
for debugging and observability. This is a silent failure path that makes diagnosing extraction
issues difficult.
Code

src/components/BookmarkReader.tsx[R40-53]

+    extractArticle(bookmark.url)
+      .then((result) => {
+        if (cancelled) return;
+        setArticle(result);
+        setStatus('done');
+        // Mark as read when content loads
+        if (!bookmark.is_read && onMarkRead) {
+          onMarkRead(bookmark.id);
+        }
+      })
+      .catch(() => {
+        if (cancelled) return;
+        setStatus('error');
+      });
Evidence
Rule 3 requires meaningful error handling with context and proper logging/monitoring; the PR
introduces a .catch(() => ...) that discards the exception and provides no diagnostic context.

Rule 3: Generic: Robust Error Handling and Edge Case Management
src/components/BookmarkReader.tsx[40-53]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Bookmark extraction failures are caught without the error object, preventing meaningful debugging and monitoring.

## Issue Context
We need actionable error context (what failed and why) while keeping user-facing messaging generic.

## Fix Focus Areas
- src/components/BookmarkReader.tsx[40-53]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


6. Editor URL input unvalidated 📘 Rule violation ⛨ Security
Description
User-provided URLs from window.prompt() are inserted directly into link/image attributes without
validation, enabling potentially dangerous schemes (e.g., javascript:) or malformed URLs. This
violates the requirement to validate and sanitize external/user inputs.
Code

src/components/SuperEditor.tsx[R437-445]

+          <ToolBtn icon={LinkIcon} label="Lien" active={editor.isActive('link')} onClick={() => {
+            const url = window.prompt('URL du lien');
+            if (url) editor.chain().focus().extendMarkRange('link').setLink({ href: url }).run();
+          }} />
+          <ToolBtn icon={Unlink} label="Supprimer lien" onClick={() => editor.chain().focus().unsetLink().run()} />
+          <ToolBtn icon={ImageIcon} label="Image" onClick={() => {
+            const url = window.prompt('URL de l\'image');
+            if (url) editor.chain().focus().setImage({ src: url }).run();
+          }} />
Evidence
Rule 6 requires input validation/sanitization; the PR takes raw user input and applies it as
href/src without scheme/format validation.

Rule 6: Generic: Security-First Input Validation and Data Handling
src/components/SuperEditor.tsx[437-445]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The editor inserts raw user-provided URLs into links/images without validation, which can enable unsafe schemes or injection.

## Issue Context
URLs should be parsed and validated (scheme allowlist) before being stored/rendered.

## Fix Focus Areas
- src/components/SuperEditor.tsx[437-445]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


7. Pandoc temp collisions 🐞 Bug ⛯ Reliability
Description
pandoc_export uses fixed filenames in a shared temp directory
(export_input.html/export_output.{ext}). Concurrent exports can overwrite each other and return
corrupted/wrong output or leak content between operations.
Code

src-tauri/src/lib.rs[R678-699]

+    let tmp_dir = std::env::temp_dir().join("superflux_pandoc");
+    std::fs::create_dir_all(&tmp_dir)
+        .map_err(|e| format!("Failed to create temp dir: {e}"))?;
+
+    let input_path = tmp_dir.join("export_input.html");
+    let ext = match format.as_str() {
+        "docx" => "docx",
+        "pdf" => "pdf",
+        other => return Err(format!("Unsupported format: {other}")),
+    };
+    let output_path = tmp_dir.join(format!("export_output.{ext}"));
+
+    std::fs::write(&input_path, &html_content)
+        .map_err(|e| format!("Failed to write temp file: {e}"))?;
+
+    let output = std::process::Command::new("pandoc")
+        .arg(input_path.to_str().unwrap())
+        .arg("-f").arg("html")
+        .arg("-t").arg(&format)
+        .arg("-o").arg(output_path.to_str().unwrap())
+        .output()
+        .map_err(|e| format!("pandoc execution failed: {e}"))?;
Evidence
The backend always writes to the same temp paths under temp_dir()/superflux_pandoc, so overlapping
invocations race on the same files.

src-tauri/src/lib.rs[676-699]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`pandoc_export` uses constant temp filenames, causing collisions under concurrent usage.

### Issue Context
Tauri commands can be invoked multiple times quickly from the UI.

### Fix Focus Areas
- src-tauri/src/lib.rs[676-713]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View more (3)
8. Inefficient base64 conversion 🐞 Bug ➹ Performance
Description
pandocService converts Uint8Array↔base64 via per-byte string concatenation plus btoa/atob. Large
files (PDF/DOCX) can cause severe UI jank, high memory use, or runtime failures due to huge
intermediate strings.
Code

src/services/pandocService.ts[R61-76]

+function uint8ToBase64(bytes: Uint8Array): string {
+  let binary = '';
+  for (let i = 0; i < bytes.length; i++) {
+    binary += String.fromCharCode(bytes[i]);
+  }
+  return btoa(binary);
+}
+
+function base64ToUint8(b64: string): Uint8Array {
+  const binary = atob(b64);
+  const bytes = new Uint8Array(binary.length);
+  for (let i = 0; i < binary.length; i++) {
+    bytes[i] = binary.charCodeAt(i);
+  }
+  return bytes;
+}
Evidence
The helpers build a single giant JS string by repeatedly concatenating in a loop and then
base64-encoding it. This is a common performance/memory footgun for multi-MB files.

src/services/pandocService.ts[61-76]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
Large imports/exports can freeze/crash due to inefficient base64 conversions.

### Issue Context
This code is used for DOCX/PDF import/export flows.

### Fix Focus Areas
- src/services/pandocService.ts[15-36]
- src/services/pandocService.ts[61-76]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


9. Help shortcut doesn't work 🐞 Bug ✓ Correctness
Description
The help command registers shortcut '?' but key matching requires shiftKey to equal the parsed
keybind’s shift flag. Pressing '?' typically sets e.shiftKey=true, while parseShortcut('?') sets
shift=false, so the shortcut never matches.
Code

src/hooks/useCommands.ts[R21-41]

+function parseShortcut(shortcut: string): KeyBind {
+  const parts = shortcut.toLowerCase().split('+');
+  return {
+    key: parts[parts.length - 1],
+    ctrl: parts.includes('ctrl'),
+    alt: parts.includes('alt'),
+    shift: parts.includes('shift'),
+    meta: parts.includes('meta'),
+  };
+}
+
+function matchesKeybind(e: KeyboardEvent, kb: KeyBind): boolean {
+  const key = e.key.toLowerCase();
+  // Handle special cases
+  const targetKey = kb.key === ',' ? ',' : kb.key === '/' ? '/' : kb.key === '?' ? '?' : kb.key;
+
+  if (key !== targetKey) return false;
+  if (!!kb.ctrl !== (e.ctrlKey || e.metaKey)) return false;
+  if (!!kb.alt !== e.altKey) return false;
+  if (!!kb.shift !== e.shiftKey) return false;
+
Evidence
App registers the shortcut as literal '?'. useCommands parses it into a keybind with key='?' and
shift=false (because no 'shift+' in the string), then matchesKeybind enforces shift equality,
preventing '?' presses (shifted '/') from matching.

src/App.tsx[773-776]
src/hooks/useCommands.ts[21-41]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
Keyboard shortcut &#x27;?&#x27; for the shortcuts overlay never triggers because the keybind parsing/matching doesn’t model the implicit Shift required to type &#x27;?&#x27;.

### Issue Context
App registers `shortcut: &#x27;?&#x27;`, but the matcher requires `kb.shift === e.shiftKey`.

### Fix Focus Areas
- src/App.tsx[773-776]
- src/hooks/useCommands.ts[21-43]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


10. Invalid URL crashes UI 🐞 Bug ⛯ Reliability
Description
BookmarkPanel/BookmarkReader call new URL(bk.url).hostname during render. If any stored bookmark URL
is not a valid absolute URL, new URL() throws and can crash the panel render.
Code

src/components/BookmarkPanel.tsx[R156-160]

+                  <div className="bk-compact-item__text">
+                    <h3 className="bk-compact-item__title">{bk.title}</h3>
+                    <div className="bk-compact-item__meta">
+                      <span className="bk-compact-item__site">{bk.site_name || new URL(bk.url).hostname}</span>
+                      <span className="bk-compact-item__sep">·</span>
Evidence
Both components assume urls are always parseable by the URL constructor, but the database schema
only enforces non-null text. A malformed record would throw at render time.

src/components/BookmarkReader.tsx[95-104]
src/components/BookmarkPanel.tsx[156-165]
supabase/migrations/20250225000005_bookmarks.sql[5-21]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`new URL(...)` in render paths can throw and crash the bookmarks UI when encountering malformed URLs.

### Issue Context
Supabase schema doesn’t enforce URL format, so records can contain invalid values.

### Fix Focus Areas
- src/components/BookmarkReader.tsx[95-104]
- src/components/BookmarkPanel.tsx[156-165]
- supabase/migrations/20250225000005_bookmarks.sql[5-21]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

ⓘ The new review experience is currently in Beta. Learn more

Grey Divider

Qodo Logo

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Feb 25, 2026

📝 Walkthrough

Walkthrough

This PR introduces a comprehensive productivity suite expansion adding rich-text editor (SuperEditor with TipTap), drawing canvas (SuperDraw), bookmark management (SuperBookmark), enhanced notes with Markdown rendering and sticky board, command palette system, Pandoc document conversion, improved theme system with AMOLED and palette support, and extensive state synchronization for editor documents, bookmarks, and notes via Supabase migrations.

Changes

Cohort / File(s) Summary
Documentation & Configuration
AGENTS.md, README.md, index.html, package.json
Updated documentation, added theme/palette support, introduced TipTap and document conversion dependencies.
Core App Architecture & Theme
src/App.tsx, src/main.tsx, src/themes/palettes.ts
Wired editor documents, bookmarks, notes, command palette, and draw modes into App state; enhanced sync on login; theme color handling for AMOLED; new palette theming system with storage and application logic.
Editor Feature
src/components/SuperEditor.tsx, src/components/EditorFileList.tsx, src/services/editorDocService.ts, supabase/migrations/20250225000006_editor_documents.sql
Rich-text editor with TipTap extensions, emoji suggestions, file I/O (import/export via Pandoc), document/folder management, and Supabase persistence layer.
Drawing Feature
src/components/SuperDraw.tsx
Self-contained canvas-based drawing tool with multiple vector primitives, tools (select, hand, shapes, text, eraser), undo/redo, resizing, keyboard shortcuts, localStorage autosave, and PNG export.
Bookmarks Feature
src/components/BookmarkPanel.tsx, src/components/BookmarkReader.tsx, src/services/bookmarkService.ts, supabase/migrations/20250225000005_bookmarks.sql
Bookmark list/grid views with filtering, read-status toggling, article extraction reader with iframe fallback, and Supabase-backed persistence with RLS.
Notes Enhancement
src/components/NotePanel.tsx, src/components/NoteStickyBoard.tsx, src/services/noteService.ts, supabase/migrations/20250225000007_notes.sql
Markdown rendering, sticky board resizing support, enhanced metadata (width/height), Supabase sync with RLS, and folder management.
UI Components & Visual Effects
src/components/GradientText.tsx, src/components/ShinyText.tsx, src/components/SpotlightCard.tsx, src/components/PalettePicker.tsx, src/components/ui/animated-theme-toggler.tsx, src/components/FeedPanel.tsx
New animated gradient/shiny text components, spotlight effect card, palette picker inline/modal variants, AMOLED theme replacement for sepia, responsive pagination by view mode, and gradient-text header styling.
Command & Navigation System
src/components/CommandPalette.tsx, src/components/ShortcutsOverlay.tsx, src/hooks/useCommands.ts
Command palette with fuzzy filtering and keyboard navigation, shortcuts overlay modal, global command registration and keybind matching with input context awareness.
Panel Integration & Layout
src/components/SourcePanel.tsx, src/hooks/useResizablePanels.ts, src/hooks/useFeedStore.ts
Added editor/draw/bookmark modes to brand switching, palette picker, bookmark URL input, editor file list integration, feed drag-and-drop reordering with visual indicators, exposed setWidths setter for dynamic panel width adjustments.
Services & Infrastructure
src/services/pandocService.ts, src/services/syncService.ts, src/services/rssService.ts, src/contexts/AuthContext.tsx
Pandoc document conversion wrapper (Tauri/web fallback via Mammoth), enhanced sync with remote deletion tracking via ID comparison, feed URL guard in RSS parsing, editor/bookmark key cleanup on auth sign-out.
Tauri Backend
src-tauri/src/lib.rs
Added three Tauri commands for Pandoc integration: pandoc_check (version check), pandoc_import (base64 file to HTML conversion), pandoc_export (HTML to docx/pdf base64 export).

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant App
    participant SuperEditor
    participant PandocService
    participant EditorDocService
    participant Supabase

    User->>App: Switch to Editor mode
    App->>SuperEditor: Load active doc
    User->>SuperEditor: Import DOCX file
    SuperEditor->>PandocService: importWithPandoc(file)
    alt Desktop (Tauri)
        PandocService->>PandocService: Encode file to base64
        PandocService->>App: invoke pandoc_import
        App-->>PandocService: Return HTML
    else Web
        PandocService->>PandocService: Use Mammoth for DOCX
    end
    PandocService-->>SuperEditor: HTML content
    SuperEditor->>App: onUpdateContent(html)
    App->>EditorDocService: updateEditorDocContent(userId, docId, html)
    EditorDocService->>Supabase: Update editor_documents
    Supabase-->>EditorDocService: Success
    EditorDocService-->>App: Confirm
    App->>User: Editor displays imported content

    User->>SuperEditor: Export to PDF
    SuperEditor->>PandocService: exportWithPandoc(html, 'pdf')
    alt Desktop (Tauri)
        PandocService->>App: invoke pandoc_export
        App-->>PandocService: Return base64 PDF
        PandocService->>PandocService: Convert to Blob
    else Web
        PandocService-->>User: Error - unavailable
    end
    PandocService-->>SuperEditor: Blob
    SuperEditor->>User: Download PDF
Loading
sequenceDiagram
    participant User
    participant App
    participant BookmarkPanel
    participant BookmarkService
    participant ArticleExtractor as ArticleExtractor<br/>(extractArticle)
    participant Supabase

    User->>App: Login
    App->>BookmarkService: fetchBookmarks(userId)
    BookmarkService->>Supabase: SELECT from bookmarks
    Supabase-->>BookmarkService: Bookmark array
    BookmarkService-->>App: Display bookmarks

    User->>BookmarkPanel: Click bookmark
    BookmarkPanel->>App: onSelectBookmark(id)
    App->>BookmarkReader: Pass bookmark
    BookmarkReader->>ArticleExtractor: extractArticle(url)
    ArticleExtractor-->>BookmarkReader: {title, content, image, date}
    BookmarkReader->>BookmarkService: toggleBookmarkRead(userId, id, true)
    BookmarkService->>Supabase: UPDATE is_read
    Supabase-->>BookmarkService: Success
    BookmarkReader->>User: Display article in reader mode

    User->>BookmarkPanel: Toggle read status
    BookmarkPanel->>BookmarkService: toggleBookmarkRead(userId, id, isRead)
    BookmarkService->>Supabase: UPDATE is_read
    Supabase-->>BookmarkPanel: Confirm
    BookmarkPanel->>User: Update UI optimistically
Loading
sequenceDiagram
    participant User
    participant App
    participant CommandPalette
    participant useCommands Hook
    participant Command Action

    User->>App: Press Ctrl+K
    App->>useCommands Hook: Global keydown listener
    useCommands Hook->>useCommands Hook: Match Ctrl+K
    useCommands Hook->>App: openPalette()
    App->>CommandPalette: isOpen=true, commands=ref
    CommandPalette->>User: Show palette, focus input

    User->>CommandPalette: Type search query
    CommandPalette->>CommandPalette: Fuzzy match against commands
    CommandPalette->>User: Display filtered results

    User->>CommandPalette: Press Enter on selected command
    CommandPalette->>App: onClose()
    App->>useCommands Hook: closePalette()
    useCommands Hook->>useCommands Hook: Find matching command
    useCommands Hook->>Command Action: Execute action
    Command Action->>App: Update state/trigger flow
    App->>User: Handle command (navigate, toggle panel, etc.)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

🐰 A rabbit hops through code so grand,
With editors, bookmarks, drawing at hand,
Commands glow bright, notes dance and sway,
SuperFlux blooms in a marvelous way! 📚✨🎨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 22.08% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'v0.6.0 — Modes, drawing canvas, bookmarks, editor & command palette' accurately summarizes the major features added: five integrated modes with drawing, bookmarks, editor, and command palette functionality.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch 0.6.0

Tip

Try Coding Plans. Let us write the prompt for your AI agent so you can ship faster (with fewer bugs).
Share your feedback on Discord.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@devohmycode devohmycode merged commit 8b01cd8 into master Feb 25, 2026
1 check was pending
Comment on lines +216 to +219
<div
className="bk-reader-body"
dangerouslySetInnerHTML={{ __html: article.content }}
/>
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

1. Unsanitized article.content html 📘 Rule violation ⛨ Security

article.content from external pages is rendered via dangerouslySetInnerHTML without
sanitization, enabling XSS if the extracted content contains scripts or malicious markup. This
violates the requirement to validate/sanitize external inputs before rendering.
Agent Prompt
## Issue description
`article.content` (external, untrusted HTML) is injected into the DOM with `dangerouslySetInnerHTML` without sanitization, creating an XSS risk.

## Issue Context
`extractArticle(bookmark.url)` fetches/parses content from arbitrary websites, so `article.content` must be treated as untrusted.

## Fix Focus Areas
- src/components/BookmarkReader.tsx[216-219]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +344 to +473
} catch (err: any) {
setImportError(err?.message || 'Erreur lors de l\'import');
}
e.target.value = '';
}, [editor, doc, onUpdateContent]);

const handleExport = useCallback(async (format: 'docx' | 'pdf') => {
if (!editor) return;
setImportError(null);
try {
const html = editor.getHTML();
const blob = await exportWithPandoc(html, format);
const title = doc?.title || 'supereditor-document';
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `${title.replace(/[^a-zA-Z0-9-_ ]/g, '')}.${format}`;
a.click();
URL.revokeObjectURL(url);
} catch (err: any) {
setImportError(err?.message || 'Erreur lors de l\'export');
}
}, [editor, doc?.title]);

// Keyboard shortcuts for file menu
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (!editor) return;
if (e.ctrlKey && e.key === 'n') { e.preventDefault(); handleNew(); }
if (e.ctrlKey && e.key === 'o') { e.preventDefault(); handleOpen(); }
if (e.ctrlKey && e.key === 's') { e.preventDefault(); handleDownload(); }
};
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [editor, handleNew, handleOpen, handleDownload]);

if (!editor) return null;

const chars = editor.storage.characterCount?.characters?.() ?? 0;
const words = editor.storage.characterCount?.words?.() ?? 0;

return (
<div className="super-editor-root">
<input
ref={fileInputRef}
type="file"
accept=".html,.htm,.txt,.md"
style={{ display: 'none' }}
onChange={handleFileChange}
/>
<input
ref={importInputRef}
type="file"
accept=".docx,.pdf"
style={{ display: 'none' }}
onChange={handleImportChange}
/>
{/* Toolbar */}
<div className="super-editor-toolbar">
<div className="super-editor-toolbar-row">
<FileMenu onNew={handleNew} onOpen={handleOpen} onDownload={handleDownload} onImport={handleImport} onExport={handleExport} />
<ToolSep />
<ToolBtn icon={Bold} label="Gras" active={editor.isActive('bold')} onClick={() => editor.chain().focus().toggleBold().run()} />
<ToolBtn icon={Italic} label="Italique" active={editor.isActive('italic')} onClick={() => editor.chain().focus().toggleItalic().run()} />
<ToolBtn icon={UnderlineIcon} label="Souligné" active={editor.isActive('underline')} onClick={() => editor.chain().focus().toggleUnderline().run()} />
<ToolBtn icon={Strikethrough} label="Barré" active={editor.isActive('strike')} onClick={() => editor.chain().focus().toggleStrike().run()} />
<ToolBtn icon={Code} label="Code" active={editor.isActive('code')} onClick={() => editor.chain().focus().toggleCode().run()} />

<ToolSep />

<ToolBtn icon={Heading1} label="H1" active={editor.isActive('heading', { level: 1 })} onClick={() => editor.chain().focus().toggleHeading({ level: 1 }).run()} />
<ToolBtn icon={Heading2} label="H2" active={editor.isActive('heading', { level: 2 })} onClick={() => editor.chain().focus().toggleHeading({ level: 2 }).run()} />
<ToolBtn icon={Heading3} label="H3" active={editor.isActive('heading', { level: 3 })} onClick={() => editor.chain().focus().toggleHeading({ level: 3 }).run()} />
<ToolBtn icon={Heading4} label="H4" active={editor.isActive('heading', { level: 4 })} onClick={() => editor.chain().focus().toggleHeading({ level: 4 }).run()} />

<ToolSep />

<ToolBtn icon={AlignLeft} label="Gauche" active={editor.isActive({ textAlign: 'left' })} onClick={() => editor.chain().focus().setTextAlign('left').run()} />
<ToolBtn icon={AlignCenter} label="Centre" active={editor.isActive({ textAlign: 'center' })} onClick={() => editor.chain().focus().setTextAlign('center').run()} />
<ToolBtn icon={AlignRight} label="Droite" active={editor.isActive({ textAlign: 'right' })} onClick={() => editor.chain().focus().setTextAlign('right').run()} />
<ToolBtn icon={AlignJustify} label="Justifié" active={editor.isActive({ textAlign: 'justify' })} onClick={() => editor.chain().focus().setTextAlign('justify').run()} />

<ToolSep />

<ToolBtn icon={List} label="Liste à puces" active={editor.isActive('bulletList')} onClick={() => editor.chain().focus().toggleBulletList().run()} />
<ToolBtn icon={ListOrdered} label="Liste numérotée" active={editor.isActive('orderedList')} onClick={() => editor.chain().focus().toggleOrderedList().run()} />
<ToolBtn icon={ListTodo} label="Liste de tâches" active={editor.isActive('taskList')} onClick={() => editor.chain().focus().toggleTaskList().run()} />
<ToolBtn icon={Quote} label="Citation" active={editor.isActive('blockquote')} onClick={() => editor.chain().focus().toggleBlockquote().run()} />
<ToolBtn icon={FileCode} label="Bloc de code" active={editor.isActive('codeBlock')} onClick={() => editor.chain().focus().toggleCodeBlock().run()} />
<ToolBtn icon={Minus} label="Séparateur" onClick={() => editor.chain().focus().setHorizontalRule().run()} />

<ToolSep />

<ToolBtn icon={LinkIcon} label="Lien" active={editor.isActive('link')} onClick={() => {
const url = window.prompt('URL du lien');
if (url) editor.chain().focus().extendMarkRange('link').setLink({ href: url }).run();
}} />
<ToolBtn icon={Unlink} label="Supprimer lien" onClick={() => editor.chain().focus().unsetLink().run()} />
<ToolBtn icon={ImageIcon} label="Image" onClick={() => {
const url = window.prompt('URL de l\'image');
if (url) editor.chain().focus().setImage({ src: url }).run();
}} />

<ToolSep />

<ToolBtn icon={SubscriptIcon} label="Indice" active={editor.isActive('subscript')} onClick={() => editor.chain().focus().toggleSubscript().run()} />
<ToolBtn icon={SuperscriptIcon} label="Exposant" active={editor.isActive('superscript')} onClick={() => editor.chain().focus().toggleSuperscript().run()} />

<ToolSep />

<ToolBtn icon={Undo2} label="Annuler" disabled={!editor.can().undo()} onClick={() => editor.chain().focus().undo().run()} />
<ToolBtn icon={Redo2} label="Rétablir" disabled={!editor.can().redo()} onClick={() => editor.chain().focus().redo().run()} />
</div>
</div>

{/* Editor content with drag handle */}
<div className="super-editor-content-wrapper">
<DragHandle editor={editor}>
<div className="super-editor-drag-handle">
<GripVertical size={14} />
</div>
</DragHandle>
<EditorContent editor={editor} className="super-editor-content" />
</div>

{/* Import/Export error toast */}
{importError && (
<div className="super-editor-toast" onClick={() => setImportError(null)}>
<span>⚠ {importError}</span>
</div>
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

2. importerror exposes raw error 📘 Rule violation ⛨ Security

The UI displays err.message directly to users via the import/export toast, which can leak internal
details (e.g., stack/context, file paths, backend error text). User-facing errors should be generic
while detailed errors go only to internal logs.
Agent Prompt
## Issue description
The SuperEditor import/export flow surfaces `err.message` directly to users, potentially leaking internal details.

## Issue Context
Compliance requires generic user-facing errors; detailed diagnostics should go to internal logs only.

## Fix Focus Areas
- src/components/SuperEditor.tsx[344-346]
- src/components/SuperEditor.tsx[469-473]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +19 to +36
export async function fetchNotes(userId: string): Promise<NoteRow[]> {
console.log('[notes] isSupabaseConfigured:', isSupabaseConfigured, 'userId:', userId);
if (!isSupabaseConfigured) { console.warn('[notes] Supabase not configured'); return []; }

const { data, error, status } = await supabase
.from('notes')
.select('*')
.eq('user_id', userId)
.order('updated_at', { ascending: false });

console.log('[notes] fetch response — status:', status, 'data:', data?.length, 'error:', error);

if (error) {
console.error('[notes] fetch error:', error);
return [];
}

return data ?? [];
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

3. fetchnotes logs userid 📘 Rule violation ⛨ Security

New console logs include userId and full error objects, which can expose sensitive identifiers and
potentially sensitive backend details in logs. Logs are also unstructured, reducing audit/debug
usefulness and violating secure logging requirements.
Agent Prompt
## Issue description
`fetchNotes()` logs sensitive identifiers (`userId`) and raw error objects to the console, and the log format is unstructured.

## Issue Context
Compliance requires logs to avoid PII/sensitive data and be structured for auditing/monitoring.

## Fix Focus Areas
- src/services/noteService.ts[19-33]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +646 to +656
fn pandoc_import(base64_data: String, filename: String) -> Result<String, String> {
let bytes = STANDARD.decode(&base64_data)
.map_err(|e| format!("base64 decode error: {e}"))?;

let tmp_dir = std::env::temp_dir().join("superflux_pandoc");
std::fs::create_dir_all(&tmp_dir)
.map_err(|e| format!("Failed to create temp dir: {e}"))?;

let input_path = tmp_dir.join(&filename);
std::fs::write(&input_path, &bytes)
.map_err(|e| format!("Failed to write temp file: {e}"))?;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

4. Pandoc path traversal 🐞 Bug ⛨ Security

pandoc_import writes a temp file using tmp_dir.join(filename) where filename is provided by the
frontend. Absolute paths or '..' segments can escape the temp directory, enabling arbitrary file
overwrite on the user’s machine.
Agent Prompt
### Issue description
`pandoc_import` uses a user-provided `filename` to build a filesystem path (`tmp_dir.join(&filename)`), allowing path traversal / absolute-path override.

### Issue Context
Frontend passes `file.name` to the backend.

### Fix Focus Areas
- src-tauri/src/lib.rs[645-674]
- src/services/pandocService.ts[15-26]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 17

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

♻️ Duplicate comments (1)
src/components/SettingsModal.tsx (1)

78-83: Duplicated color-mapping logic — already noted in main.tsx review.

Same amoled/dark/light branching; same refactor recommendation applies.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/SettingsModal.tsx` around lines 78 - 83, The color-mapping
logic that computes isAmoled/isDark and r,g,b is duplicated; extract it into a
reusable helper (e.g., a function named getThemeRGB or mapThemeToRGB) and
replace the inlined block in SettingsModal (the isAmoled/isDark checks and r,g,b
assignments) with a call to that helper; ensure the helper accepts the document
element or reads document.documentElement and returns an {r,g,b} object so
main.tsx and SettingsModal.tsx can both call the same function to avoid
duplication.
🟡 Minor comments (11)
AGENTS.md-12-25 (1)

12-25: ⚠️ Potential issue | 🟡 Minor

Add fenced code block languages (markdownlint MD040).

Line 12 and Line 21 code fences should declare a language (e.g., bash) to satisfy linting.

📝 Proposed fix
-```
+```bash
 npm run dev          # Full Tauri dev mode (Vite + Rust backend)
 npm run dev:app      # Frontend only (Vite on port 5173, no Tauri)
 npm run dev:proxy    # CORS proxy server on port 3001 (for browser-only dev)

@@
- +bash
npm run build # Full Tauri build (frontend + Rust → installer)
npm run build:frontend # TypeScript check + Vite build only
npm run lint # ESLint on all .ts/.tsx files

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@AGENTS.md` around lines 12 - 25, Add explicit language identifiers to the
fenced code blocks in AGENTS.md: update the first code block (the dev scripts
block shown with npm run dev/dev:app/dev:proxy) to use ```bash and update the
second code block (the Build & Lint block with npm run build/build:frontend/npm
run lint) to use ```bash so the markdown linter MD040 is satisfied; edit the two
fenced blocks in AGENTS.md accordingly.
src/components/SpotlightCard.tsx-39-41 (1)

39-41: ⚠️ Potential issue | 🟡 Minor

Avoid initial top-left spotlight flash on hover.

Line 40 sets opacity before pointer position is initialized for the current hover event, so the first frame can render at (0,0).

💡 Proposed fix
-  const handleMouseEnter = () => {
-    setOpacity(0.6);
-  };
+  const handleMouseEnter: React.MouseEventHandler<HTMLDivElement> = e => {
+    if (divRef.current) {
+      const rect = divRef.current.getBoundingClientRect();
+      setPosition({ x: e.clientX - rect.left, y: e.clientY - rect.top });
+    }
+    setOpacity(0.6);
+  };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/SpotlightCard.tsx` around lines 39 - 41, The initial opacity
is being set in handleMouseEnter before the pointer coordinates are initialized,
causing a flash at (0,0); modify handleMouseEnter to accept the mouse event,
derive and set the spotlight coordinates (e.g., update spotlightX/spotlightY or
call setSpotlightPosition using event.clientX/Y or relative element coordinates)
first, then setOpacity(0.6) (or set opacity inside requestAnimationFrame after
coordinates are set) so the first rendered frame uses the correct pointer
position; reference handleMouseEnter and setOpacity (and the spotlight
coordinate state setters) when making the change.
src/services/pandocService.ts-29-33 (1)

29-33: ⚠️ Potential issue | 🟡 Minor

Case-sensitive file extension check may reject .DOCX files.

file.name.endsWith('.docx') won't match uppercase or mixed-case extensions (e.g., .DOCX, .Docx), which some Windows users may have. Consider normalizing the name.

🔧 Proposed fix
-  if (file.name.endsWith('.docx')) {
+  if (file.name.toLowerCase().endsWith('.docx')) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/services/pandocService.ts` around lines 29 - 33, The extension check
`file.name.endsWith('.docx')` is case-sensitive and will miss `.DOCX`/`.Docx`;
update the condition in pandocService.ts to perform a case-insensitive check
(e.g., normalize with `file.name.toLowerCase()` or use a case-insensitive regex)
before calling `mammoth.convertToHtml({ arrayBuffer: buffer })`, so any
capitalization of `.docx` is accepted.
supabase/migrations/20250225000006_editor_documents.sql-5-14 (1)

5-14: ⚠️ Potential issue | 🟡 Minor

created_at and updated_at lack NOT NULL constraints.

Both columns default to now() but aren't declared NOT NULL, so an explicit INSERT ... (created_at) VALUES (NULL) would succeed. If these should always be populated, add NOT NULL.

🔧 Suggested fix
-  created_at timestamptz default now(),
-  updated_at timestamptz default now(),
+  created_at timestamptz not null default now(),
+  updated_at timestamptz not null default now(),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@supabase/migrations/20250225000006_editor_documents.sql` around lines 5 - 14,
The created_at and updated_at columns in the editor_documents table are missing
NOT NULL and therefore can be set to NULL despite having defaults; alter the
CREATE TABLE definition for editor_documents to declare created_at timestamptz
NOT NULL default now() and updated_at timestamptz NOT NULL default now() so
those columns (created_at, updated_at) cannot be inserted as NULL.
src/components/SuperDraw.tsx-357-368 (1)

357-368: ⚠️ Potential issue | 🟡 Minor

Resize can produce negative w / h when handles are dragged past each other.

When dragging the nw handle far past the se corner (or vice-versa), w and h can become negative. This will cause hitTest, paintSel, and getHandles to return incorrect results since they assume non-negative dimensions. Consider normalizing after resize:

🔧 Suggested normalization
         return { ...el, x, y, w, h };
+        // After computing x, y, w, h:
+        const nx = w < 0 ? x + w : x;
+        const ny = h < 0 ? y + h : y;
+        return { ...el, x: nx, y: ny, w: Math.abs(w), h: Math.abs(h) };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/SuperDraw.tsx` around lines 357 - 368, The resize branch in
the setEls mapping can produce negative w/h when handles cross; normalize
width/height and adjust x/y after computing new values so downstream code
(hitTest, paintSel, getHandles) always sees non-negative dimensions: inside the
cur.type === 'resize' case in the setEls callback (the closure that maps el => {
... }), after computing x, y, w, h apply normalization (if w < 0 then x += w; w
= Math.abs(w); if h < 0 then y += h; h = Math.abs(h)) and return the normalized
{ ...el, x, y, w, h } so other functions can assume non-negative sizes.
src/components/BookmarkReader.tsx-103-103 (1)

103-103: ⚠️ Potential issue | 🟡 Minor

new URL(bookmark.url) can throw on malformed URLs.

If bookmark.url is invalid, this will throw and crash the component during render.

Proposed fix
-          <span className="bk-reader-site">{bookmark.site_name || new URL(bookmark.url).hostname}</span>
+          <span className="bk-reader-site">{bookmark.site_name || (() => { try { return new URL(bookmark.url).hostname; } catch { return bookmark.url; } })()}</span>

Or extract a helper:

function safeHostname(url: string): string {
  try { return new URL(url).hostname; } catch { return url; }
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/BookmarkReader.tsx` at line 103, The render uses new
URL(bookmark.url) which can throw for malformed URLs; update BookmarkReader.tsx
to avoid throwing by replacing the inline new URL call with a safe helper (e.g.,
safeHostname) that wraps new URL(bookmark.url) in a try/catch and returns a
fallback (like the original url or empty string) on error, and use that helper
where you currently reference bookmark.site_name || new
URL(bookmark.url).hostname.
src/components/GradientText.tsx-66-74 (1)

66-74: ⚠️ Potential issue | 🟡 Minor

diagonal direction produces the same backgroundPosition as horizontal.

The else branch (line 72) is ${p}% 50%, identical to the horizontal branch. For a diagonal motion the vertical component should also vary.

🐛 Proposed fix
     if (direction === 'horizontal') {
       return `${p}% 50%`;
     } else if (direction === 'vertical') {
       return `50% ${p}%`;
     } else {
-      return `${p}% 50%`;
+      return `${p}% ${p}%`;
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/GradientText.tsx` around lines 66 - 74, The backgroundPosition
useTransform handler incorrectly treats the "diagonal" case the same as
"horizontal"; update the else branch in the backgroundPosition calculation
(inside the useTransform for progress) so that when direction === 'diagonal'
both X and Y components vary (e.g., use p for both axes) instead of returning
the horizontal value; adjust the conditional to explicitly handle 'diagonal' or
change the default return to compute `${p}% ${p}%` so diagonal motion changes
both coordinates.
src/hooks/useCommands.ts-32-43 (1)

32-43: ⚠️ Potential issue | 🟡 Minor

meta-only keybinds silently never match; targetKey ternary is a no-op.

  1. Line 35: The ternary chain always returns kb.key unchanged — it can be simplified to const targetKey = kb.key;.

  2. Line 38 merges ctrlKey || metaKey into a single check against kb.ctrl, but kb.meta is never checked. A command registered with meta: true, ctrl: false will never match because pressing Meta makes (e.ctrlKey || e.metaKey) true while !!kb.ctrl is false. This is fine today since all shortcuts use Ctrl+…, but it's a latent bug if anyone registers a Meta-only binding.

🛡️ Proposed fix
 function matchesKeybind(e: KeyboardEvent, kb: KeyBind): boolean {
   const key = e.key.toLowerCase();
-  // Handle special cases
-  const targetKey = kb.key === ',' ? ',' : kb.key === '/' ? '/' : kb.key === '?' ? '?' : kb.key;
-
-  if (key !== targetKey) return false;
-  if (!!kb.ctrl !== (e.ctrlKey || e.metaKey)) return false;
+  if (key !== kb.key) return false;
+  const wantCtrlOrMeta = !!kb.ctrl || !!kb.meta;
+  const hasCtrlOrMeta = e.ctrlKey || e.metaKey;
+  if (wantCtrlOrMeta !== hasCtrlOrMeta) return false;
   if (!!kb.alt !== e.altKey) return false;
   if (!!kb.shift !== e.shiftKey) return false;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/hooks/useCommands.ts` around lines 32 - 43, The matchesKeybind function
incorrectly builds targetKey with a no-op ternary and conflates Ctrl and Meta
into a single check, so meta-only bindings never match; simplify targetKey to
use kb.key directly and change the modifier checks to test e.ctrlKey and
e.metaKey separately against kb.ctrl and kb.meta (i.e., verify !!kb.ctrl ===
e.ctrlKey, !!kb.meta === e.metaKey, and !!kb.alt === e.altKey, !!kb.shift ===
e.shiftKey) inside matchesKeybind so Meta-only, Ctrl-only, and combined
modifiers all match correctly.
src/components/SuperEditor.tsx-372-375 (1)

372-375: ⚠️ Potential issue | 🟡 Minor

Shortcut handling excludes macOS Cmd keys.

At Lines 372-374, only Ctrl is handled. Cmd+N/O/S won’t work on macOS.

Proposed fix
   useEffect(() => {
     const handleKeyDown = (e: KeyboardEvent) => {
       if (!editor) return;
-      if (e.ctrlKey && e.key === 'n') { e.preventDefault(); handleNew(); }
-      if (e.ctrlKey && e.key === 'o') { e.preventDefault(); handleOpen(); }
-      if (e.ctrlKey && e.key === 's') { e.preventDefault(); handleDownload(); }
+      const mod = e.ctrlKey || e.metaKey;
+      const key = e.key.toLowerCase();
+      if (mod && key === 'n') { e.preventDefault(); handleNew(); }
+      if (mod && key === 'o') { e.preventDefault(); handleOpen(); }
+      if (mod && key === 's') { e.preventDefault(); handleDownload(); }
     };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/SuperEditor.tsx` around lines 372 - 375, The shortcut checks
only look for Ctrl keys, so macOS Command shortcuts won't work; update the
keydown handler in SuperEditor.tsx to accept either e.ctrlKey or e.metaKey
(e.g., use if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === 'n') {
e.preventDefault(); handleNew(); } and similarly for handleOpen() and
handleDownload()), ensuring you normalize e.key to lowercase and still call
preventDefault().
src/components/SourcePanel.tsx-885-905 (1)

885-905: ⚠️ Potential issue | 🟡 Minor

+ action is inconsistent in draw mode (wrong title and no action path).

In draw mode, the tooltip falls back to bookmark wording, and click handling has no draw branch.

Proposed fix
         <button
           className="footer-btn footer-btn-add"
           title={
             brandMode === 'flux' ? 'Ajouter un flux' :
             brandMode === 'note' ? 'Nouvelle note' :
             brandMode === 'editor' ? 'Nouveau document' :
-            'Ajouter un bookmark'
+            brandMode === 'bookmark' ? 'Ajouter un bookmark' :
+            'Aucune action rapide en mode dessin'
           }
+          disabled={brandMode === 'draw'}
           onClick={() => {
             if (brandMode === 'flux') {
@@
             } else if (brandMode === 'bookmark') {
               setBookmarkUrlOpen(prev => !prev);
               setTimeout(() => bookmarkUrlRef.current?.focus(), 50);
+            } else if (brandMode === 'draw') {
+              return;
             }
           }}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/SourcePanel.tsx` around lines 885 - 905, The tooltip/title and
click handler need a draw branch: update the title expression (the title prop
using brandMode) to include brandMode === 'draw' with the correct label (e.g.,
"Nouveau dessin" or whatever the UI copy requires) instead of falling back to
bookmark text, and add a matching branch in the onClick handler for brandMode
=== 'draw' that invokes the draw action (e.g., open the draw modal/state —
reference the same mechanism you use for other modals such as setIsAddModalOpen
or an existing draw-specific setter or callback). Ensure you reference brandMode
in both places and use the existing modal/callback helpers (setIsAddModalOpen,
onAddNote, onAddDoc, setBookmarkUrlOpen, bookmarkUrlRef) to implement the draw
behavior consistently with the other branches.
src/components/NoteStickyBoard.tsx-141-146 (1)

141-146: ⚠️ Potential issue | 🟡 Minor

Persist final resize dimensions from pointer coordinates, not potentially stale state.

At Line 145, onResizeEnd(size.w, size.h) can save stale values if the last setSize from Line 135-138 hasn’t committed yet.

Proposed fix
+  const clampSize = useCallback((w: number, h: number) => ({
+    w: Math.max(MIN_STICKY_W, Math.min(MAX_STICKY_W, w)),
+    h: Math.max(MIN_STICKY_H, Math.min(MAX_STICKY_H, h)),
+  }), []);
+
   const handleResizePointerMove = useCallback((e: React.PointerEvent) => {
     if (!isResizing) return;
     const dx = e.clientX - resizeStart.current.mouseX;
     const dy = e.clientY - resizeStart.current.mouseY;
-    setSize({
-      w: Math.max(MIN_STICKY_W, Math.min(MAX_STICKY_W, resizeStart.current.w + dx)),
-      h: Math.max(MIN_STICKY_H, Math.min(MAX_STICKY_H, resizeStart.current.h + dy)),
-    });
-  }, [isResizing]);
+    setSize(clampSize(resizeStart.current.w + dx, resizeStart.current.h + dy));
+  }, [isResizing, clampSize]);
 
   const handleResizePointerUp = useCallback((e: React.PointerEvent) => {
     if (!isResizing) return;
     (e.target as HTMLElement).releasePointerCapture(e.pointerId);
+    const dx = e.clientX - resizeStart.current.mouseX;
+    const dy = e.clientY - resizeStart.current.mouseY;
+    const next = clampSize(resizeStart.current.w + dx, resizeStart.current.h + dy);
+    setSize(next);
     setIsResizing(false);
-    onResizeEnd(size.w, size.h);
-  }, [isResizing, size, onResizeEnd]);
+    onResizeEnd(next.w, next.h);
+  }, [isResizing, onResizeEnd, clampSize]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/NoteStickyBoard.tsx` around lines 141 - 146,
handleResizePointerUp currently calls onResizeEnd(size.w, size.h) which can read
stale React state; update the handler to derive the final dimensions from the
pointer event or a ref that is updated during pointer moves. Concretely, either
compute final width/height from the pointer event (e.g., using
e.clientX/e.clientY and the note element's getBoundingClientRect) or maintain a
lastSizeRef that you update alongside setSize inside the pointer move handler,
then call onResizeEnd(lastSizeRef.current.w, lastSizeRef.current.h) in
handleResizePointerUp; keep the existing calls to releasePointerCapture and
setIsResizing(false) in handleResizePointerUp.
🧹 Nitpick comments (19)
src/services/rssService.ts (1)

359-360: LGTM — correct early-return guard.

The !feed.url short-circuit already covers null, undefined, and ''; the additional feed.url.trim() === '' clause extends coverage to whitespace-only strings, which is a sensible defensive touch. Placement before any network I/O is correct.

One optional simplification — the two conditions can be collapsed:

🔧 Optional simplification
-  if (!feed.url || feed.url.trim() === '') return [];
+  if (!feed.url?.trim()) return [];
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/services/rssService.ts` around lines 359 - 360, Collapse the two
early-return checks into a single null/empty/whitespace guard to simplify the
logic: replace the separate "!feed.url || feed.url.trim() === ''" checks with a
single expression that trims safely (e.g., using optional chaining or a
null-safe check) and returns early when the result is falsy; update the guard
around the feed processing code where "feed.url" is validated so that
whitespace-only URLs are still rejected while avoiding redundant checks.
src/hooks/useResizablePanels.ts (1)

99-103: Pass-through setter wrapper can be simplified.

Line 99–Line 101 only proxies setWidths; either return setWidths directly or add clamping/normalization here to enforce panel invariants.

♻️ Minimal simplification
-  const setWidthsOverride = useCallback((newWidths: [number, number, number]) => {
-    setWidths(newWidths);
-  }, []);
-
-  return { widths, setWidths: setWidthsOverride, handleMouseDown, containerRef };
+  return { widths, setWidths, handleMouseDown, containerRef };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/hooks/useResizablePanels.ts` around lines 99 - 103, The small wrapper
setWidthsOverride in useResizablePanels simply proxies setWidths; either remove
it and return setWidths directly in the return object, or implement
normalization/clamping inside setWidthsOverride (e.g., clamp each value and
enforce any invariants like min/max or total sum) before calling setWidths;
update the returned object to use the chosen function name (keep containerRef,
handleMouseDown and widths as-is) and ensure setWidthsOverride references the
same tuple type [number, number, number].
src/components/FeedPanel.tsx (1)

334-342: Consider whether a continuously-animating gradient on the panel title is desired.

GradientText runs a requestAnimationFrame loop for its gradient animation. Since this title is always mounted while the panel is open, it will consume rAF cycles continuously. If the animation is meant to be subtle/decorative, consider adding pauseOnHover={false} or limiting the animation duration to avoid unnecessary battery/CPU usage on long sessions.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/FeedPanel.tsx` around lines 334 - 342, The GradientText
component used for the panel title continuously runs rAF; to avoid unnecessary
CPU/battery use, update the FeedPanel usage of GradientText (in the JSX block
rendering {title}) to either disable continuous animation by passing
pauseOnHover={false} or reduce the animation runtime by increasing
animationSpeed (or both) so the gradient is less taxing; modify the props on the
GradientText instance (the component named GradientText in this file)
accordingly to limit or pause the animation for long-lived mounts.
src/components/SuperDraw.tsx (1)

175-177: Assigning to R.current during render is flagged by React 19's ref rules.

This is a known pattern for syncing state to an imperative ref, but React's strict mode and the react-hooks/refs rule now explicitly flag it. The recommended approach is to use a layout effect:

♻️ Proposed fix
   const R = useRef({ els, cam, sel });
-  R.current = { els, cam, sel };
+  // Sync after render, before paint
+  useEffect(() => {
+    R.current = { els, cam, sel };
+  });

Since paint already runs after state changes via the repaint useEffect, using a no-deps useEffect here keeps the ref in sync without violating the render-purity rule.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/SuperDraw.tsx` around lines 175 - 177, The ref R (created via
useRef({ els, cam, sel }) and currently reassigned with R.current = { els, cam,
sel } during render) must not be mutated during render; move the R.current
update into a no-deps effect so the ref mirrors state after render. Replace the
direct assignment with a useEffect (or useLayoutEffect if synchronous update
before paint is required) that sets R.current = { els, cam, sel }, ensuring
existing consumers like paint and the repaint useEffect read from R.current as
before.
src/services/pandocService.ts (1)

61-67: Performance concern: O(n²) string concatenation for large files.

For large DOCX/PDF files (potentially tens of MB), building binary via += in a loop creates a new string on every iteration. Consider using a chunk-based approach or the built-in Uint8Array→base64 path available in modern runtimes.

♻️ Suggested improvement
 function uint8ToBase64(bytes: Uint8Array): string {
-  let binary = '';
-  for (let i = 0; i < bytes.length; i++) {
-    binary += String.fromCharCode(bytes[i]);
-  }
-  return btoa(binary);
+  const CHUNK = 0x8000;
+  const parts: string[] = [];
+  for (let i = 0; i < bytes.length; i += CHUNK) {
+    parts.push(String.fromCharCode(...bytes.subarray(i, i + CHUNK)));
+  }
+  return btoa(parts.join(''));
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/services/pandocService.ts` around lines 61 - 67, The uint8ToBase64
function builds a binary string via repeated += causing O(n²) work for large
files; replace the loop with a direct binary->base64 conversion appropriate to
the runtime (e.g., use Buffer.from(bytes).toString('base64') in Node or a
chunked approach that uses String.fromCharCode on slices to build a single
binary string before calling btoa in browsers). Update the uint8ToBase64
implementation to use the Buffer-based conversion or a chunked
String.fromCharCode approach to avoid per-byte concatenation and improve
performance.
src/main.tsx (1)

15-20: Duplicated color-mapping logic between main.tsx and SettingsModal.tsx.

Lines 15–20 here and lines 78–83 in SettingsModal.tsx contain identical AMOLED/dark/light color mapping. Consider extracting a shared helper (e.g., getEffectColors()) to avoid divergence if one is updated without the other.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main.tsx` around lines 15 - 20, Extract the duplicated AMOLED/dark/light
color mapping into a single exported helper named getEffectColors() that returns
an object { r, g, b } by checking
document.documentElement.classList.contains('amoled') and 'dark' (use the same
logic currently in main.tsx/SettingsModal.tsx); then replace the inline logic in
both main.tsx and SettingsModal.tsx with calls to getEffectColors() and use the
returned r,g,b values, keeping the helper in a shared module (e.g., utils or
helpers) so both files import it and avoid future divergence.
src/components/ShortcutsOverlay.tsx (1)

34-56: Consider adding basic accessibility attributes to the overlay.

The modal-like overlay lacks role, aria-modal, and aria-label attributes. This would improve screen reader support.

♻️ Suggested improvement
-    <div className="shortcuts-backdrop" onClick={onClose}>
-      <div className="shortcuts-overlay" onClick={e => e.stopPropagation()}>
+    <div className="shortcuts-backdrop" onClick={onClose} role="presentation">
+      <div className="shortcuts-overlay" role="dialog" aria-modal="true" aria-label="Raccourcis clavier" onClick={e => e.stopPropagation()}>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/ShortcutsOverlay.tsx` around lines 34 - 56, Add basic ARIA
attributes to the overlay rendered by the ShortcutsOverlay component: set
role="dialog" and aria-modal="true" on the element with class
"shortcuts-overlay" (or the main container acting as the modal) and provide an
accessible name via aria-label or aria-labelledby (pointing to the h2 with class
"shortcuts-title"); ensure the backdrop element with class "shortcuts-backdrop"
still handles onClick={onClose} and the close button remains focusable so screen
readers can discover and dismiss the dialog.
index.html (1)

14-16: AMOLED theme does not add the dark class — fragile for downstream consumers.

When theme === 'amoled', only the amoled class is added. Any CSS rule or JS check that targets .dark alone will miss AMOLED mode. The JS code in main.tsx and SettingsModal.tsx already works around this with isAmoled || isDark checks, but CSS selectors and any future code would need to duplicate selectors (.dark, .amoled).

Consider also adding dark when amoled is set, so AMOLED is treated as a dark-mode superset:

♻️ Suggested change
       if (t === 'amoled') {
+        document.documentElement.classList.add('dark');
         document.documentElement.classList.add('amoled');
       } else if (t === 'dark' || (!t && window.matchMedia('(prefers-color-scheme: dark)').matches)) {

This way, .dark CSS rules apply universally and .amoled can override with pure-black backgrounds.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@index.html` around lines 14 - 16, When theme is set to 'amoled' ensure the
document root gets both classes so dark-mode rules still apply: in the block
that currently handles t === 'amoled' (where
document.documentElement.classList.add('amoled') is called) also add the 'dark'
class (i.e., add both 'amoled' and 'dark'); likewise ensure the theme-switching
logic that removes or toggles classes keeps 'dark' and 'amoled' consistent
(remove both when switching away from amoled and only remove 'amoled' when
switching to dark) so downstream CSS/JS that targets '.dark' continues to work.
src/components/PalettePicker.tsx (2)

38-40: Hoist isDark computation out of the .map() loop.

isDark reads from document.documentElement.classList which doesn't change between iterations. Computing it once before the map avoids redundant DOM reads on every palette item.

Same applies to the PalettePickerInline component at line 74.

Proposed fix (PalettePicker)
+      const isDark = document.documentElement.classList.contains('dark') || document.documentElement.classList.contains('amoled');
       {palettes.map(p => {
-          const isDark = document.documentElement.classList.contains('dark') || document.documentElement.classList.contains('amoled');
           const colors = isDark ? p.dark : p.light;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/PalettePicker.tsx` around lines 38 - 40, Hoist the DOM read
for theme detection so you compute isDark once per render instead of inside each
iteration: move const isDark =
document.documentElement.classList.contains('dark') ||
document.documentElement.classList.contains('amoled') out of the palettes.map
callback in the PalettePicker component and do the same in the
PalettePickerInline component (the isDark computation referenced around line 74)
so both components read document.documentElement.classList only once and then
use that const inside their map callbacks to select p.dark or p.light.

62-95: Palette item rendering is duplicated between PalettePicker and PalettePickerInline.

The two components share nearly identical rendering logic for palette items. Consider extracting a shared PaletteItem or a PaletteList sub-component to reduce duplication, if this surface area is expected to evolve.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/PalettePicker.tsx` around lines 62 - 95, PalettePicker and
PalettePickerInline duplicate the same palette item rendering; refactor by
extracting a shared sub-component (e.g., PaletteItem or PaletteList) that
renders a single palette button and accepts props like palette (Palette),
isActive (boolean), onSelect ((palette: Palette) => void) and current theme
detection logic; replace the inline map in both PalettePicker and
PalettePickerInline to call the new component and forward
applyPalette/handleSelect logic (keep existing functions like
PalettePickerInline, handleSelect, applyPalette, palettes, currentId) so
behavior and state updates remain unchanged.
src/components/BookmarkReader.tsx (1)

68-79: Retry handler lacks cancellation — stale state updates possible.

Unlike the main useEffect fetch (which sets a cancelled flag on cleanup), handleRetry fires an uncancellable promise. If the bookmark changes or the component unmounts mid-retry, setArticle / setStatus will update stale state. React 19 won't warn, but the user could briefly see the wrong article's content flash.

Consider reusing the effect-based fetch by resetting prevUrlRef.current and forcing a re-trigger via a counter state, or adding a similar cancellation ref.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/BookmarkReader.tsx` around lines 68 - 79, The retry handler
handleRetry currently starts an uncancellable extractArticle(bookmark.url)
promise which can call setArticle/setStatus after the bookmark changed or
component unmounted; change it to either (A) reuse the existing effect-based
fetch by incrementing a retry counter state (e.g. retryCount) and resetting
prevUrlRef.current so the useEffect that contains the cancelled flag will run
and handle cleanup, or (B) add a cancellation ref (like cancelledRef) and check
it in the .then/.catch callbacks (and set cancelledRef on unmount) so
setArticle/setStatus are skipped when cancelled; update references to
prevUrlRef, handleRetry, extractArticle, setArticle and setStatus accordingly.
src/services/syncService.ts (1)

352-352: Long compound filter condition hurts readability.

This single line packs 4 conditions. Consider extracting to a named predicate for clarity.

Proposed refactor
-    const itemsToPush = mergedItems.filter(i => !remoteItemIdSet.has(i.id) && !(i.url && remoteItemUrlSet.has(i.url)) && !deletedItemIds.has(i.id) && !deletedFeedIds.has(i.feedId));
+    const itemsToPush = mergedItems.filter(i => {
+      if (remoteItemIdSet.has(i.id)) return false;      // already on remote
+      if (i.url && remoteItemUrlSet.has(i.url)) return false; // URL match on remote
+      if (deletedItemIds.has(i.id)) return false;        // deleted remotely
+      if (deletedFeedIds.has(i.feedId)) return false;    // parent feed deleted remotely
+      return true;
+    });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/services/syncService.ts` at line 352, The filter on mergedItems assigned
to itemsToPush is a long compound condition that hurts readability; extract the
predicate into a well-named function (e.g., isPushableItem) and use it in
mergedItems.filter to make intent clear. Specifically, implement a function that
accepts an item and returns the boolean composed of the four checks against
remoteItemIdSet, remoteItemUrlSet, deletedItemIds, and deletedFeedIds, then
replace the inline arrow predicate in the itemsToPush assignment with a call to
that function (referencing itemsToPush, mergedItems, and the sets
remoteItemIdSet/remoteItemUrlSet/deletedItemIds/deletedFeedIds).
src/components/CommandPalette.tsx (1)

92-143: Mutable flatIdx counter in render body is functional but fragile.

Using a let flatIdx = 0 that gets incremented inside nested .map() callbacks relies on synchronous render execution. It works correctly in React's current model, but if the rendering logic were ever refactored (e.g., extracted into sub-components), the counter would break.

An alternative is to pre-compute a commandToFlatIndex map from flatList:

const cmdIndexMap = useMemo(() => new Map(flatList.map((c, i) => [c.id, i])), [flatList]);

This is a minor nit — the current approach works fine.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/CommandPalette.tsx` around lines 92 - 143, The render uses a
mutable let flatIdx incremented inside nested .map callbacks (flatIdx) which is
fragile; replace this by precomputing a stable mapping from command id to flat
index (e.g., compute cmdIndexMap from flatList with useMemo) and then look up
idx = cmdIndexMap.get(cmd.id) when rendering the grouped entries, leaving
selectedIndex, setSelectedIndex, grouped, flatList, and cmd.id references
intact; ensure the map is memoized on flatList so indices stay consistent across
renders and remove the mutable flatIdx variable.
src/components/NotePanel.tsx (1)

124-130: Consider truncating content before Markdown parsing in card view.

Each card renders the full note.content through react-markdown. For a grid with many notes or notes with large content, this could cause noticeable rendering lag since every card incurs a full Markdown parse.

Consider passing a truncated preview (e.g., first 200 chars) to <Markdown> in card view, reserving full rendering for the editor.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/NotePanel.tsx` around lines 124 - 130, NotePanel currently
sends the full note.content into the <Markdown> renderer for each card, causing
heavy parsing; change the card rendering to pass a truncated preview instead
(e.g., first 200 chars with ellipsis) so Markdown receives preview text in the
card view and full content is only rendered in the editor; implement this by
adding a small helper (e.g., truncatePreview) or inline logic and use it where
note.content is passed to <Markdown> in the NotePanel card rendering block to
replace note.content with the preview variable.
src/services/editorDocService.ts (2)

3-10: EditorDocRow omits user_id — intentional?

The Supabase query uses .select('*'), which returns user_id, but the EditorDocRow interface doesn't include it. This is fine if you deliberately want to keep user IDs out of the UI layer, but if any caller ever needs to inspect the owner, the type won't reflect the actual shape. A quick user_id?: string or a comment explaining the omission would clarify intent.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/services/editorDocService.ts` around lines 3 - 10, EditorDocRow currently
omits the user_id field returned by Supabase SELECT '*'; update the EditorDocRow
interface to include user_id?: string (or user_id: string if required) or add a
comment on the EditorDocRow declaration explaining the omission so the type
matches the actual payload from the DB and callers can access the owner when
needed; locate the EditorDocRow interface in src/services/editorDocService.ts
and adjust the type or add the explanatory comment accordingly.

93-111: updateEditorDocMeta will send an empty payload if called with {}.

If meta is {} (neither title nor folder provided), Supabase receives an empty update which may no-op or error depending on the version. A guard would make this defensive.

🛡️ Proposed guard
 export async function updateEditorDocMeta(
   userId: string,
   docId: string,
   meta: { title?: string; folder?: string | null }
 ): Promise<boolean> {
   if (!isSupabaseConfigured) return false;
+  if (!meta.title && meta.folder === undefined) return true; // nothing to update

   const { error } = await supabase
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/services/editorDocService.ts` around lines 93 - 111, The function
updateEditorDocMeta currently calls supabase.update even when meta is an empty
object; add a guard at the top of updateEditorDocMeta to detect an empty payload
(e.g., meta is falsy or Object.keys(meta).length === 0) and short-circuit
(return true or another no-op indicator) so you never call
supabase.from('editor_documents').update(meta) with an empty object; update the
function body around the meta parameter check to skip the database call when
there's nothing to update.
src/components/EditorFileList.tsx (2)

155-158: Sorting is recomputed on every render — consider useMemo.

sorted and rootDocs are derived on each render. For a large doc list this is unnecessary work; wrapping in useMemo keyed on docs avoids repeated allocations.

♻️ Proposed change
-  const sorted = [...docs].sort((a, b) =>
-    new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()
-  );
-  const rootDocs = sorted.filter(d => !d.folder);
+  const sorted = useMemo(
+    () => [...docs].sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()),
+    [docs]
+  );
+  const rootDocs = useMemo(() => sorted.filter(d => !d.folder), [sorted]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/EditorFileList.tsx` around lines 155 - 158, The sorting and
filtering of docs are recomputed on every render (variables sorted and rootDocs
in EditorFileList), causing unnecessary allocations; wrap the creation of sorted
and rootDocs in a useMemo keyed on docs so the sort and filter run only when
docs changes (reference the sorted and rootDocs variables inside the useMemo
callback and return both values for use in the component). Ensure you import
useMemo from React and replace direct declarations of sorted and rootDocs with
the memoized result.

16-38: Exporting utility functions alongside the component breaks React Fast Refresh.

loadEditorDocs, saveEditorDocs, loadEditorFolders, and saveEditorFolders are non-component exports that cause the Fast Refresh plugin to skip HMR for this entire module during development. Consider moving these four helpers to a dedicated file (e.g., editorDocStorage.ts).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/EditorFileList.tsx` around lines 16 - 38, Exported
non-component helpers loadEditorDocs, saveEditorDocs, loadEditorFolders, and
saveEditorFolders in the EditorFileList module prevent React Fast Refresh from
working; move these four functions into a separate module (e.g., a new editor
storage module), export them from there, and update EditorFileList to import and
use those functions instead of exporting them itself; ensure function signatures
and STORAGE_KEY/FOLDERS_KEY references remain unchanged and update any
tests/imports accordingly.
src/components/BookmarkPanel.tsx (1)

46-58: Optimistic UI updates are not rolled back on service failure.

handleRemove removes the bookmark from state before removeBookmark resolves. If the network call fails, the user loses the item from the UI without it being deleted server-side (same for handleToggleRead). Consider reverting the optimistic state on failure or at least notifying the user.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/BookmarkPanel.tsx` around lines 46 - 58, The optimistic
updates in handleRemove and handleToggleRead update state before the async
service calls and do not revert on failure; capture the previous bookmarks state
(e.g., const prev = bookmarks or from setState functional previous), perform the
optimistic setBookmarks change, then await removeBookmark(user.id, id) or
toggleBookmarkRead(user.id, id, isRead) inside a try/catch, and on catch restore
the previous bookmarks with setBookmarks(prev) and surface an error (e.g., call
an existing notify/showError function or set an error state) so the UI is rolled
back when the network call fails; modify handleRemove and handleToggleRead to
follow this pattern and reference setBookmarks, removeBookmark,
toggleBookmarkRead, and user.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 498d69c and 5242d91.

⛔ Files ignored due to path filters (2)
  • package-lock.json is excluded by !**/package-lock.json
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (39)
  • AGENTS.md
  • README.md
  • index.html
  • package.json
  • src-tauri/src/lib.rs
  • src/App.tsx
  • src/components/BookmarkPanel.tsx
  • src/components/BookmarkReader.tsx
  • src/components/CommandPalette.tsx
  • src/components/EditorFileList.tsx
  • src/components/FeedPanel.tsx
  • src/components/GradientText.tsx
  • src/components/NotePanel.tsx
  • src/components/NoteStickyBoard.tsx
  • src/components/PalettePicker.tsx
  • src/components/SettingsModal.tsx
  • src/components/ShinyText.tsx
  • src/components/ShortcutsOverlay.tsx
  • src/components/SourcePanel.tsx
  • src/components/SpotlightCard.tsx
  • src/components/SuperDraw.tsx
  • src/components/SuperEditor.tsx
  • src/components/ui/animated-theme-toggler.tsx
  • src/contexts/AuthContext.tsx
  • src/hooks/useCommands.ts
  • src/hooks/useFeedStore.ts
  • src/hooks/useResizablePanels.ts
  • src/index.css
  • src/main.tsx
  • src/services/bookmarkService.ts
  • src/services/editorDocService.ts
  • src/services/noteService.ts
  • src/services/pandocService.ts
  • src/services/rssService.ts
  • src/services/syncService.ts
  • src/themes/palettes.ts
  • supabase/migrations/20250225000005_bookmarks.sql
  • supabase/migrations/20250225000006_editor_documents.sql
  • supabase/migrations/20250225000007_notes.sql

Comment on lines +646 to +656
fn pandoc_import(base64_data: String, filename: String) -> Result<String, String> {
let bytes = STANDARD.decode(&base64_data)
.map_err(|e| format!("base64 decode error: {e}"))?;

let tmp_dir = std::env::temp_dir().join("superflux_pandoc");
std::fs::create_dir_all(&tmp_dir)
.map_err(|e| format!("Failed to create temp dir: {e}"))?;

let input_path = tmp_dir.join(&filename);
std::fs::write(&input_path, &bytes)
.map_err(|e| format!("Failed to write temp file: {e}"))?;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Sanitize filename before constructing temp paths (path traversal risk).

Line 654 joins untrusted filename directly. Absolute paths or .. segments can escape the intended temp directory and overwrite arbitrary files.

🔐 Proposed fix
+    let safe_name = std::path::Path::new(&filename)
+        .file_name()
+        .and_then(|n| n.to_str())
+        .ok_or("Invalid filename")?;
-    let input_path = tmp_dir.join(&filename);
+    let input_path = tmp_dir.join(safe_name);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
fn pandoc_import(base64_data: String, filename: String) -> Result<String, String> {
let bytes = STANDARD.decode(&base64_data)
.map_err(|e| format!("base64 decode error: {e}"))?;
let tmp_dir = std::env::temp_dir().join("superflux_pandoc");
std::fs::create_dir_all(&tmp_dir)
.map_err(|e| format!("Failed to create temp dir: {e}"))?;
let input_path = tmp_dir.join(&filename);
std::fs::write(&input_path, &bytes)
.map_err(|e| format!("Failed to write temp file: {e}"))?;
fn pandoc_import(base64_data: String, filename: String) -> Result<String, String> {
let bytes = STANDARD.decode(&base64_data)
.map_err(|e| format!("base64 decode error: {e}"))?;
let tmp_dir = std::env::temp_dir().join("superflux_pandoc");
std::fs::create_dir_all(&tmp_dir)
.map_err(|e| format!("Failed to create temp dir: {e}"))?;
let safe_name = std::path::Path::new(&filename)
.file_name()
.and_then(|n| n.to_str())
.ok_or("Invalid filename")?;
let input_path = tmp_dir.join(safe_name);
std::fs::write(&input_path, &bytes)
.map_err(|e| format!("Failed to write temp file: {e}"))?;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src-tauri/src/lib.rs` around lines 646 - 656, The pandoc_import function
currently joins untrusted filename directly into tmp_dir (input_path =
tmp_dir.join(&filename)), allowing absolute paths or ".." segments to escape the
temp directory; fix this by validating and sanitizing filename before joining:
reject or strip absolute path prefixes, ensure you only use the path's basename
(e.g., Path::new(&filename).file_name()), forbid or normalize "."/".." segments,
fall back to a safe generated filename (UUID or timestamp) if the basename is
missing or empty, and then construct input_path = tmp_dir.join(sanitized_name);
update error handling to report invalid filenames from pandoc_import.

Comment on lines +678 to +689
let tmp_dir = std::env::temp_dir().join("superflux_pandoc");
std::fs::create_dir_all(&tmp_dir)
.map_err(|e| format!("Failed to create temp dir: {e}"))?;

let input_path = tmp_dir.join("export_input.html");
let ext = match format.as_str() {
"docx" => "docx",
"pdf" => "pdf",
other => return Err(format!("Unsupported format: {other}")),
};
let output_path = tmp_dir.join(format!("export_output.{ext}"));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Use per-request temp files to prevent collisions and data leaks.

Line 682 and Line 688 use deterministic filenames in a shared temp directory. Concurrent exports can clobber each other and return incorrect/cross-user output.

🧪 Proposed fix (unique request directory)
+    let request_id = std::time::SystemTime::now()
+        .duration_since(std::time::UNIX_EPOCH)
+        .map_err(|e| format!("clock error: {e}"))?
+        .as_nanos();
+    let request_dir = tmp_dir.join(format!("job-{request_id}"));
+    std::fs::create_dir_all(&request_dir)
+        .map_err(|e| format!("Failed to create request temp dir: {e}"))?;
-
-    let input_path = tmp_dir.join("export_input.html");
+    let input_path = request_dir.join("export_input.html");
@@
-    let output_path = tmp_dir.join(format!("export_output.{ext}"));
+    let output_path = request_dir.join(format!("export_output.{ext}"));
@@
-    let _ = std::fs::remove_file(&output_path);
+    let _ = std::fs::remove_file(&output_path);
+    let _ = std::fs::remove_dir_all(&request_dir);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src-tauri/src/lib.rs` around lines 678 - 689, The current code creates
deterministic temp files (tmp_dir, input_path, output_path) which can collide
across concurrent requests; change to create a unique per-request temp directory
or file names (e.g., by using a UUID or tempfile::Builder) and use that
directory for input_path and output_path, ensuring you still call
std::fs::create_dir_all (or let tempfile create the dir) and clean up after use;
update references to tmp_dir, input_path, and output_path in the export function
to use the newly generated unique path.

Comment on lines +157 to +158
const contentSaveTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const noteSaveTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Single debounce timer per entity type can drop remote writes across different notes/docs.

If the user edits doc A then doc B quickly, the timer for A is canceled by B. Same issue for notes. This risks unsynced backend state.

Proposed fix
-  const contentSaveTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
-  const noteSaveTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
+  const contentSaveTimersRef = useRef(new Map<string, ReturnType<typeof setTimeout>>());
+  const noteSaveTimersRef = useRef(new Map<string, ReturnType<typeof setTimeout>>());
@@
-      if (contentSaveTimerRef.current) clearTimeout(contentSaveTimerRef.current);
-      contentSaveTimerRef.current = setTimeout(() => {
+      const prevTimer = contentSaveTimersRef.current.get(id);
+      if (prevTimer) clearTimeout(prevTimer);
+      const timer = setTimeout(() => {
         updateEditorDocContent(user.id, id, content);
+        contentSaveTimersRef.current.delete(id);
       }, 1000);
+      contentSaveTimersRef.current.set(id, timer);
@@
-        if (noteSaveTimerRef.current) clearTimeout(noteSaveTimerRef.current);
-        noteSaveTimerRef.current = setTimeout(() => {
+        const prevTimer = noteSaveTimersRef.current.get(noteId);
+        if (prevTimer) clearTimeout(prevTimer);
+        const timer = setTimeout(() => {
           updateNoteContent(user.id, noteId, updates.content!);
+          noteSaveTimersRef.current.delete(noteId);
         }, 1000);
+        noteSaveTimersRef.current.set(noteId, timer);
       }
@@
+  useEffect(() => {
+    return () => {
+      contentSaveTimersRef.current.forEach(clearTimeout);
+      noteSaveTimersRef.current.forEach(clearTimeout);
+    };
+  }, []);

Also applies to: 227-231, 324-327

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/App.tsx` around lines 157 - 158, The current single refs
contentSaveTimerRef and noteSaveTimerRef cause timers to be overwritten when
switching documents/notes, dropping pending saves; change both to maps keyed by
entity id (e.g., contentSaveTimerRef: useRef<Record<string, ReturnType<typeof
setTimeout> | null>> and noteSaveTimerRef similarly) and update the debounce
logic (where save timers are set/cleared) to clear/set timers using the current
doc/note id instead of a single shared ref; update all usages (including the
other occurrences around the indicated sections) to use the id-keyed timer
lookup so each document/note has its own debounce timer and remote writes are
not lost.

Comment on lines 350 to +378
const handleRenameNoteFolder = useCallback((oldName: string, newName: string) => {
setNoteFolders(prev => prev.map(f => f === oldName ? newName : f));
setNotes(prev => prev.map(n => n.folder === oldName ? { ...n, folder: newName } : n));
setNotes(prev => {
const updated = prev.map(n => n.folder === oldName ? { ...n, folder: newName } : n);
// Sync folder rename to Supabase for affected notes
if (user) {
updated.filter(n => n.folder === newName).forEach(n => {
updateNoteMeta(user.id, n.id, { folder: newName });
});
}
return updated;
});
if (selectedNoteFolder === oldName) setSelectedNoteFolder(newName);
}, [selectedNoteFolder]);
}, [selectedNoteFolder, user]);

const handleDeleteNoteFolder = useCallback((name: string) => {
setNoteFolders(prev => prev.filter(f => f !== name));
setNotes(prev => prev.map(n => n.folder === name ? { ...n, folder: undefined } : n));
setNotes(prev => {
const updated = prev.map(n => n.folder === name ? { ...n, folder: undefined } : n);
// Sync folder removal to Supabase for affected notes
if (user) {
updated.filter(n => !n.folder).forEach(n => {
updateNoteMeta(user.id, n.id, { folder: null });
});
}
return updated;
});
if (selectedNoteFolder === name) setSelectedNoteFolder(null);
}, [selectedNoteFolder]);
}, [selectedNoteFolder, user]);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Folder rename/delete sync updates too many notes in Supabase.

Current logic updates every note matching the post-update condition (e.g., all root notes on delete), not only notes affected by this folder operation.

Proposed fix
   const handleRenameNoteFolder = useCallback((oldName: string, newName: string) => {
+    const affectedIds = notes.filter(n => n.folder === oldName).map(n => n.id);
     setNoteFolders(prev => prev.map(f => f === oldName ? newName : f));
-    setNotes(prev => {
-      const updated = prev.map(n => n.folder === oldName ? { ...n, folder: newName } : n);
-      // Sync folder rename to Supabase for affected notes
-      if (user) {
-        updated.filter(n => n.folder === newName).forEach(n => {
-          updateNoteMeta(user.id, n.id, { folder: newName });
-        });
-      }
-      return updated;
-    });
+    setNotes(prev => prev.map(n => n.folder === oldName ? { ...n, folder: newName } : n));
+    if (user) affectedIds.forEach(id => updateNoteMeta(user.id, id, { folder: newName }));
     if (selectedNoteFolder === oldName) setSelectedNoteFolder(newName);
-  }, [selectedNoteFolder, user]);
+  }, [selectedNoteFolder, user, notes]);
 
   const handleDeleteNoteFolder = useCallback((name: string) => {
+    const affectedIds = notes.filter(n => n.folder === name).map(n => n.id);
     setNoteFolders(prev => prev.filter(f => f !== name));
-    setNotes(prev => {
-      const updated = prev.map(n => n.folder === name ? { ...n, folder: undefined } : n);
-      // Sync folder removal to Supabase for affected notes
-      if (user) {
-        updated.filter(n => !n.folder).forEach(n => {
-          updateNoteMeta(user.id, n.id, { folder: null });
-        });
-      }
-      return updated;
-    });
+    setNotes(prev => prev.map(n => n.folder === name ? { ...n, folder: undefined } : n));
+    if (user) affectedIds.forEach(id => updateNoteMeta(user.id, id, { folder: null }));
     if (selectedNoteFolder === name) setSelectedNoteFolder(null);
-  }, [selectedNoteFolder, user]);
+  }, [selectedNoteFolder, user, notes]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/App.tsx` around lines 350 - 378, In handleRenameNoteFolder and
handleDeleteNoteFolder the Supabase sync filters use the post-update condition
(checking n.folder === newName or !n.folder) which matches too many notes;
instead capture the affected notes from the previous state before mapping and
only call updateNoteMeta for those (e.g., inside setNotes(prev => { const
affected = prev.filter(n => n.folder === oldName) ...; /* map to updated */; if
(user) affected.forEach(n => updateNoteMeta(user.id, n.id, { folder: newName }))
})). Do the analogous change in handleDeleteNoteFolder (capture prev.filter(n =>
n.folder === name) and update those IDs with folder: null). This ensures
updateNoteMeta is only called for notes that actually had the target folder.

<div className="bk-compact-item__text">
<h3 className="bk-compact-item__title">{bk.title}</h3>
<div className="bk-compact-item__meta">
<span className="bk-compact-item__site">{bk.site_name || new URL(bk.url).hostname}</span>
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

new URL(bk.url) can throw on malformed URLs, crashing the render.

If any bookmark has an invalid url value, new URL(bk.url).hostname will throw a TypeError, which will propagate through React's render and crash the component. The same pattern also appears at line 218.

🛡️ Proposed fix — extract a safe helper
+function safeHostname(url: string): string {
+  try { return new URL(url).hostname; }
+  catch { return url; }
+}
+
 function formatDate(dateStr: string): string {

Then replace both occurrences:

-<span className="bk-compact-item__site">{bk.site_name || new URL(bk.url).hostname}</span>
+<span className="bk-compact-item__site">{bk.site_name || safeHostname(bk.url)}</span>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/BookmarkPanel.tsx` at line 159, The render uses new
URL(bk.url).hostname directly (in the BookmarkPanel component inside the span
with class bk-compact-item__site and the other occurrence later) which can throw
for malformed URLs; add a small safe helper function (e.g., getHostnameSafe or
parseHostname) that accepts a string, returns the hostname on success or a
sensible fallback (empty string or the original input) on failure, implemented
with a try/catch and guards for null/undefined, then replace both occurrences of
new URL(bk.url).hostname with a call to that helper and keep the existing
bk.site_name || ... fallback logic intact.

Comment on lines +55 to +65
if (event.key === 'ArrowUp') {
setSelectedIndex(i => (i + props.items.length - 1) % props.items.length);
return true;
}
if (event.key === 'ArrowDown') {
setSelectedIndex(i => (i + 1) % props.items.length);
return true;
}
if (event.key === 'Enter') {
const item = props.items[selectedIndex];
if (item) props.command(item);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Emoji keyboard navigation is inconsistent with rendered list (and breaks on empty list).

You navigate over props.items.length but render only props.items.slice(0, 12). This lets selection move to hidden entries; with zero items it also risks invalid modulo math.

Proposed fix
 const EmojiList = forwardRef<
   { onKeyDown: (props: { event: KeyboardEvent }) => boolean },
   { items: EmojiItem[]; command: (item: EmojiItem) => void }
 >((props, ref) => {
   const [selectedIndex, setSelectedIndex] = useState(0);
+  const visibleItems = props.items.slice(0, 12);
+  const visibleCount = visibleItems.length;
 
   useEffect(() => { setSelectedIndex(0); }, [props.items]);
 
   useImperativeHandle(ref, () => ({
     onKeyDown: ({ event }: { event: KeyboardEvent }) => {
+      if (!visibleCount) return false;
       if (event.key === 'ArrowUp') {
-        setSelectedIndex(i => (i + props.items.length - 1) % props.items.length);
+        setSelectedIndex(i => (i + visibleCount - 1) % visibleCount);
         return true;
       }
       if (event.key === 'ArrowDown') {
-        setSelectedIndex(i => (i + 1) % props.items.length);
+        setSelectedIndex(i => (i + 1) % visibleCount);
         return true;
       }
       if (event.key === 'Enter') {
-        const item = props.items[selectedIndex];
+        const item = visibleItems[selectedIndex];
         if (item) props.command(item);
         return true;
       }
       return false;
     },
-  }));
+  }), [visibleCount, visibleItems, selectedIndex, props.command]);
 
-  if (!props.items.length) {
+  if (!visibleCount) {
     return (
       <div className="super-editor-emoji-popup">
         <span className="super-editor-emoji-empty">Aucun emoji</span>
       </div>
     );
   }
 
   return (
     <div className="super-editor-emoji-popup">
-      {props.items.slice(0, 12).map((item, index) => (
+      {visibleItems.map((item, index) => (

Also applies to: 82-83

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/SuperEditor.tsx` around lines 55 - 65, The keyboard navigation
uses props.items.length while the rendered list is props.items.slice(0, 12),
causing selection of non-visible items and breaking on empty lists; update the
handlers in SuperEditor (where setSelectedIndex and the Enter logic are used) to
compute and use a visibleItems constant (e.g., const visibleItems =
props.items.slice(0, 12)) and use visibleItems.length for the modulo arithmetic
and bounds checks, guard against visibleItems.length === 0 before doing modulo
or indexing, and apply the same change to the other handlers referenced around
the 82-83 region so all keyboard navigation and Enter selection operate on the
rendered subset only.

Comment on lines +18 to 23
document.documentElement.classList.remove("dark", "amoled")
if (theme === "dark") {
document.documentElement.classList.add("dark")
} else if (theme === "amoled") {
document.documentElement.classList.add("amoled")
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Clear legacy sepia class during theme apply.

Line 18 removes only dark/amoled. Upgraded users with a stale sepia class can end up with conflicting theme classes.

🧹 Proposed fix
-  document.documentElement.classList.remove("dark", "amoled")
+  document.documentElement.classList.remove("dark", "amoled", "sepia")
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
document.documentElement.classList.remove("dark", "amoled")
if (theme === "dark") {
document.documentElement.classList.add("dark")
} else if (theme === "amoled") {
document.documentElement.classList.add("amoled")
}
document.documentElement.classList.remove("dark", "amoled", "sepia")
if (theme === "dark") {
document.documentElement.classList.add("dark")
} else if (theme === "amoled") {
document.documentElement.classList.add("amoled")
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/ui/animated-theme-toggler.tsx` around lines 18 - 23, The
theme-apply block in animated-theme-toggler.tsx currently removes only "dark"
and "amoled" but leaves a stale "sepia" class; update the class removal to also
remove "sepia" so theme switches don’t leave conflicting classes. Locate the
document.documentElement.classList.remove call in the theme application code
(the block that checks theme === "dark" / "amoled") and include "sepia" in the
list of classes to remove before adding the new theme class.

Comment on lines +94 to +102
export function generateBookmarkId(url: string): string {
// Simple hash for desktop (sync compatible with Chrome extension's bk- prefix)
let hash = 0;
for (let i = 0; i < url.length; i++) {
const chr = url.charCodeAt(i);
hash = ((hash << 5) - hash) + chr;
hash |= 0;
}
return `bk-${Math.abs(hash).toString(16).padStart(16, '0').slice(0, 16)}`;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

generateBookmarkId has a 32-bit hash — collision risk with upsert semantics.

The hash is a 32-bit integer (via hash |= 0), producing at most ~4 billion unique IDs. Combined with the upsert in addBookmark (line 45-48, onConflict: 'id,user_id'), two different URLs that collide will silently overwrite each other. With a few thousand bookmarks per user, the birthday-problem probability becomes non-trivial.

Consider using crypto.randomUUID() or at minimum a longer hash (e.g., SHA-256 truncated) to reduce collision likelihood. If sync compatibility with the Chrome extension is the constraint, the extension's ID generation should also be upgraded.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/services/bookmarkService.ts` around lines 94 - 102, The current
generateBookmarkId function uses a 32-bit integer hash which risks collisions
that can cause silent overwrites when addBookmark uses upsert (onConflict:
'id,user_id'); replace generateBookmarkId with a collision‑resistant ID strategy
such as using crypto.randomUUID() for new IDs (or compute a SHA‑256 of the URL
and truncate to a safe length) so IDs are far less likely to collide, and update
any Chrome extension counterpart to use the same scheme for sync compatibility;
ensure addBookmark's upsert behavior remains unchanged but now operates on
globally-unique IDs.

Comment on lines +20 to +21
console.log('[notes] isSupabaseConfigured:', isSupabaseConfigured, 'userId:', userId);
if (!isSupabaseConfigured) { console.warn('[notes] Supabase not configured'); return []; }
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Remove user-identifying debug logs from note service.

Line 20 logs userId directly. This is sensitive identifier data and should not be emitted in normal client logs.

Proposed fix
 export async function fetchNotes(userId: string): Promise<NoteRow[]> {
-  console.log('[notes] isSupabaseConfigured:', isSupabaseConfigured, 'userId:', userId);
   if (!isSupabaseConfigured) { console.warn('[notes] Supabase not configured'); return []; }
@@
-  console.log('[notes] fetch response — status:', status, 'data:', data?.length, 'error:', error);
-
   if (error) {
     console.error('[notes] fetch error:', error);
     return [];
   }

Also applies to: 29-33

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/services/noteService.ts` around lines 20 - 21, The debug log in
noteService.ts prints a sensitive user identifier (userId) via console.log (and
similar logs around lines 29-33); remove the userId from those debug statements
in functions that interact with Supabase (e.g., where isSupabaseConfigured and
userId are logged), replacing them with non-identifying info such as a boolean
or redacted value (e.g., "user present" or "<redacted>") or remove the console
logs entirely and, if necessary, emit only context-level logs via the app logger
(e.g., processLogger) without any PII; ensure all occurrences in the get/list
note-related functions are updated.

Comment on lines +38 to +39
if not exists (select 1 from pg_policies where tablename = 'notes' and policyname = 'Users can update own notes') then
create policy "Users can update own notes" on notes for update using (auth.uid() = user_id);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find all migration files and check for UPDATE policies
fd -e sql . supabase/migrations/ --exec basename {} \; | sort

Repository: devohmycode/SuperFlux

Length of output: 278


🏁 Script executed:

#!/bin/bash
# Check the specific notes migration file for UPDATE policies
cat supabase/migrations/20250225000007_notes.sql

Repository: devohmycode/SuperFlux

Length of output: 2601


🏁 Script executed:

#!/bin/bash
# Search for all UPDATE policies across migration files to identify the pattern
rg "create policy.*update" -i supabase/migrations/ -A 1 -B 1

Repository: devohmycode/SuperFlux

Length of output: 2515


UPDATE policies across multiple migrations missing WITH CHECK clause — user_id could be reassigned.

The UPDATE policies in this migration (and several others: bookmarks, editor_documents, profiles, user_settings, feeds, feed_items) only specify USING (auth.uid() = user_id), which restricts which rows can be targeted. Without a WITH CHECK clause, users can update the user_id column to a different value, effectively transferring ownership of records to other users. This violates RLS intent.

The INSERT policies correctly use WITH CHECK (auth.uid() = user_id) — UPDATE policies need the same.

🔒 Fix for notes migration
-    create policy "Users can update own notes" on notes for update using (auth.uid() = user_id);
+    create policy "Users can update own notes" on notes for update using (auth.uid() = user_id) with check (auth.uid() = user_id);

Apply the same pattern across all other affected migrations.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if not exists (select 1 from pg_policies where tablename = 'notes' and policyname = 'Users can update own notes') then
create policy "Users can update own notes" on notes for update using (auth.uid() = user_id);
if not exists (select 1 from pg_policies where tablename = 'notes' and policyname = 'Users can update own notes') then
create policy "Users can update own notes" on notes for update using (auth.uid() = user_id) with check (auth.uid() = user_id);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@supabase/migrations/20250225000007_notes.sql` around lines 38 - 39, The
UPDATE policy creation for "Users can update own notes" on table notes currently
only uses USING (auth.uid() = user_id) which allows changing user_id; modify the
CREATE POLICY statement to include WITH CHECK (auth.uid() = user_id) so updates
are only valid when the resulting row still belongs to the same auth.uid(), and
apply the same change to the analogous UPDATE policy lines for bookmarks,
editor_documents, profiles, user_settings, feeds, and feed_items migrations to
prevent ownership reassignment.

@coderabbitai coderabbitai bot mentioned this pull request Mar 5, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant