Skip to content

Add interactive file tree and code block components#99

Merged
remcostoeten merged 0 commit intodaddyfrom
claude/merge-editor-file-tree-Mlqld
Feb 12, 2026
Merged

Add interactive file tree and code block components#99
remcostoeten merged 0 commit intodaddyfrom
claude/merge-editor-file-tree-Mlqld

Conversation

@remcostoeten
Copy link
Owner

@remcostoeten remcostoeten commented Feb 12, 2026

Summary

This PR introduces two new BlockNote custom blocks for the editor: an interactive file tree viewer and an enhanced code block component. These replace the previous implementation and provide better organization, accessibility, and user experience.

Key Changes

File Tree Block (file-tree/)

  • New interactive file tree component with folder expansion, file selection, and content preview
  • Configuration modal for editing tree structure, adding/removing files and folders, and managing content
  • File content viewer with syntax highlighting support for code files and markdown preview
  • Resizable panels for adjusting tree and content viewer proportions
  • Import/export functionality for JSON-based tree configurations
  • Comprehensive utilities for tree operations (build, flatten, update, delete, search)
  • Type-safe implementation with full TypeScript support
  • Accessibility features including keyboard navigation and ARIA labels
  • Unit tests for core utility functions

Code Block Component (code-block/)

  • New dedicated code block replacing the previous custom implementation
  • Language selector dropdown with 25+ supported programming languages
  • Syntax highlighting using react-syntax-highlighter
  • Editable code content with auto-resizing textarea
  • Copy-to-clipboard functionality with visual feedback
  • Filename input for better code organization
  • Language-specific icons for visual identification
  • Keyboard shortcuts for common operations
  • Responsive design with mobile support

Integration Updates

  • Updated useCustomSlashMenu hook to include both new blocks in the slash menu
  • Updated useEditorConfig to use the new codeBlockSpec
  • Added fileTreeBlock helper function to preseed data
  • Added demo content for file tree in preseed data

Implementation Details

  • Modular architecture: Each component is self-contained with clear separation of concerns
  • Context-based state management: File tree uses React Context for efficient state sharing
  • Resizable panels: Implemented with custom resize handles for flexible layouts
  • Type safety: Comprehensive TypeScript types for all components and utilities
  • Accessibility: ARIA labels, keyboard navigation, and semantic HTML throughout
  • Performance: Memoized components and optimized re-renders using useCallback

https://claude.ai/code/session_01FjPLm1j6uM2hov3GsEfVk7

Summary by Sourcery

Introduce dedicated code and file-tree blocks to the editor, with syntax-highlighted code rendering, configurable file tree viewing, and updated service worker assets to support the new functionality.

New Features:

  • Add a custom code block type with editable content, language selection, filename metadata, and syntax-highlighted rendering using react-syntax-highlighter.
  • Add an interactive file tree block with expandable folders, file selection, content preview, and a configuration modal for managing the underlying tree data.

Enhancements:

  • Integrate the new code block into the editor schema and slash menu, including a markdown-style ``` trigger for quick insertion.
  • Add theme variables and styling hooks for consistent syntax highlighting in light and dark modes.
  • Preseed the app with a demo note showcasing the interactive file tree block.
  • Update the service worker script and precache manifest to include new client bundles related to the editor and syntax highlighting.

Build:

  • Add react-syntax-highlighter and its type definitions as application dependencies.

Tests:

  • Add unit test scaffolding for file tree utilities to validate core tree operations.

Summary by CodeRabbit

  • New Features
    • Code blocks: inline editor with syntax highlighting, language selector, markdown shortcut (``` + space), and copy-to-clipboard with feedback
    • Inline code: visual variants picker, improved styling, and keyboard/pointer accessibility for switching variants
    • Slash-menu entry to insert preconfigured code snippets
    • Interactive file tree: expandable folders, keyboard navigation, resizable panels, file viewer, import/export and in‑UI config editor
    • Demo note pre-seeded showcasing the interactive file tree

@vercel
Copy link
Contributor

vercel bot commented Feb 12, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
skriuw Ready Ready Preview, Comment Feb 12, 2026 2:25pm

@chatgpt-codex-connector
Copy link

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@sourcery-ai
Copy link
Contributor

sourcery-ai bot commented Feb 12, 2026

Reviewer's Guide

Introduce two new BlockNote custom blocks—an interactive file tree and an enhanced code block—wire them into the editor schema and slash menu, and add supporting styling, utilities, and demo content, while also updating the service worker pre-cache manifest.

Sequence diagram for markdown ``` trigger inserting a code block

sequenceDiagram
    actor User
    participant DOM as EditorDOMElement
    participant InputRule as registerCodeBlockTrigger
    participant Editor as BlockNoteEditor

    User->>DOM: type "```"
    DOM-->>InputRule: keydown ` events
    InputRule->>InputRule: buffer = "```"

    User->>DOM: press Space
    DOM-->>InputRule: keydown Space
    InputRule->>InputRule: detect buffer === "```"
    InputRule->>Editor: getTextCursorPosition()
    Editor-->>InputRule: currentBlock (paragraph with "```")
    InputRule->>Editor: updateBlock(blockId, { type: codeBlock, props: { language: typescript, fileName: untitled.ts, code: "" }, content: undefined })
    InputRule->>InputRule: buffer = ""

    Editor-->>User: renders new CodeBlock with focused editor
Loading

Class diagram for new code block BlockNote block

classDiagram
    direction LR

    class CodeBlockSpec {
        <<blockSpec>>
        +string type = codeBlock
        +object propSchema
        +string content = none
    }

    class TCodeBlockProps {
        +string language
        +string fileName
        +string code
    }

    class CodeBlockRender {
        +language: string
        +fileName: string
        +code: string
        +boolean isEditing
        +string localCode
        +string localFileName
        +render(block, editor)
        +handleLanguageChange(newLanguage: string)
        +handleBlur(focusEvent: FocusEvent)
        +handleFileNameChange(changeEvent: ChangeEvent)
        +handleFileNameBlur()
        +handleFileNameKeyDown(keyboardEvent: KeyboardEvent)
        +handleContainerClick()
        +handleContainerKeyDown(keyboardEvent: KeyboardEvent)
    }

    class LanguageSelector {
        +string value
        +onChange(language: string)
        +boolean isOpen
        +string searchQuery
        +number highlightedIndex
        +handleSelect(language: string)
        +handleKeyDown(keyboardEvent: KeyboardEvent)
    }

    class CodeEditor {
        +string value
        +language: string
        +onChange(value: string)
        +onBlur()
        +onFocus()
        +boolean autoFocus
        +handleKeyDown(keyboardEvent: KeyboardEvent)
        +handleChange(changeEvent: ChangeEvent)
        +adjustHeight()
    }

    class CodeRenderer {
        +string code
        +string language
        +string fileName
        +boolean showLineNumbers
        +onCopy(code: string)
        +onClick()
        +copyToClipboard(mouseEvent: MouseEvent)
    }

    class LanguageIcon {
        +language: string
        +number size
        +string className
    }

    class TLanguage {
        <<type alias>>
    }

    class getLanguageConfig {
        +getLanguageConfig(value: string) TLanguage
    }

    class getDefaultFilename {
        +getDefaultFilename(language: string) string
    }

    class registerCodeBlockTrigger {
        +registerCodeBlockTrigger(editor: BlockNoteEditor, containerElement: HTMLElement)
    }

    class BlockNoteEditor {
        <<external>>
        +getTextCursorPosition()
        +updateBlock(blockId: string, update: object)
        +removeBlocks(blockIds: string[])
    }

    CodeBlockSpec --> TCodeBlockProps : uses props
    CodeBlockSpec --> CodeBlockRender : render

    CodeBlockRender --> BlockNoteEditor : updates blocks
    CodeBlockRender --> LanguageSelector : composes
    CodeBlockRender --> CodeEditor : composes
    CodeBlockRender --> CodeRenderer : composes
    CodeBlockRender --> getDefaultFilename : uses

    LanguageSelector --> TLanguage : selects
    LanguageSelector --> LanguageIcon : displays
    LanguageSelector --> getLanguageConfig : uses

    CodeRenderer --> LanguageIcon : displays

    getDefaultFilename --> getLanguageConfig : uses

    registerCodeBlockTrigger --> BlockNoteEditor : inserts codeBlock
Loading

Class diagram for new file tree BlockNote block

classDiagram
    direction LR

    class FileTreeBlockSpec {
        <<blockSpec>>
        +string type = fileTree
        +object propSchema
        +string content = none
    }

    class TFileTreeBlockProps {
        +string content
        +TStyle style
        +boolean showIndentLines
        +boolean initialExpandedAll
        +boolean locked
    }

    class TFile {
        +string path
        +string content
        +string language
    }

    class TNode {
        +string id
        +string name
        +string type file|folder
        +string path
        +TNode[] children
        +string content
        +string language
        +boolean isExpanded
    }

    class TComponent {
        +string name
        +string version
        +boolean showIndentLines
        +boolean enableHoverHighlight
        +TFile[] files
    }

    class TTreeState {
        +Set~string~ expandedFolders
        +string selectedFilePath
    }

    class TStyle {
        <<enum>>
        +card
        +minimal
        +full
    }

    class TreeProvider {
        +TTreeState state
        +boolean showIndentLines
        +boolean enableHoverHighlight
        +onToggleExpand(id: string)
        +onSelectFile(path: string, content: string, language: string)
    }

    class Tree {
        +TNode[] nodes
        +string ariaLabel
        +number focusedIndex
        +handleKeyDown(keyboardEvent: KeyboardEvent)
    }

    class TreeNodeComponent {
        +TNode node
        +number depth
        +string focusedPath
        +onFocus()
        +handleClick()
        +handleKeyDown(keyboardEvent: KeyboardEvent)
    }

    class FileViewer {
        +TNode selectedNode
        +string highlightedCode
        +boolean copied
        +handleCopy()
    }

    class ConfigModal {
        +boolean isOpen
        +TComponent component
        +TNode[] nodes
        +TNode selectedNode
        +string editingContent
        +string componentName
        +string componentVersion
        +handleNodeUpdate(id: string, updates: Partial~TNode~)
        +handleNodeDelete(id: string)
        +handleAddChild(parentId: string, type: string)
        +handleAddRoot(type: string)
        +handleContentChange(content: string)
        +handleSave()
        +handleExportJson()
        +handleImportJson()
    }

    class buildTreeFromFiles {
        +buildTreeFromFiles(files: TFile[]) TNode[]
    }

    class flattenTreeToFiles {
        +flattenTreeToFiles(nodes: TNode[]) TFile[]
    }

    class updateNode {
        +updateNode(nodes: TNode[], id: string, updates: Partial~TNode~) TNode[]
    }

    class deleteNode {
        +deleteNode(nodes: TNode[], id: string) TNode[]
    }

    class addChildNode {
        +addChildNode(nodes: TNode[], parentId: string, child: TNode) TNode[]
    }

    class findNodeById {
        +findNodeById(nodes: TNode[], id: string) TNode
    }

    class findNodeByPath {
        +findNodeByPath(nodes: TNode[], path: string) TNode
    }

    class serializeTreeToAscii {
        +serializeTreeToAscii(nodes: TNode[], depth: number, isLast: boolean[]) string
    }

    class parseAsciiTree {
        +parseAsciiTree(content: string) TNode[]
    }

    class getLanguageFromPath {
        +getLanguageFromPath(path: string) string
    }

    class getFileColor {
        +getFileColor(path: string) string
    }

    class BlockNoteEditorFT {
        <<external>>
        +updateBlock(blockId: string, update: object)
    }

    FileTreeBlockSpec --> TFileTreeBlockProps : uses props
    FileTreeBlockSpec --> TComponent : serialized as JSON
    FileTreeBlockSpec --> TreeProvider : render
    FileTreeBlockSpec --> Tree : render
    FileTreeBlockSpec --> FileViewer : render
    FileTreeBlockSpec --> ConfigModal : opens

    TreeProvider --> TTreeState : manages
    TreeProvider --> TNode : provides

    Tree --> TreeNodeComponent : composes

    ConfigModal --> TComponent : edits
    ConfigModal --> TNode : edits
    ConfigModal --> buildTreeFromFiles : uses
    ConfigModal --> flattenTreeToFiles : uses
    ConfigModal --> updateNode : uses
    ConfigModal --> deleteNode : uses
    ConfigModal --> addChildNode : uses
    ConfigModal --> findNodeById : uses

    FileViewer --> TNode : displays
    FileViewer --> getLanguageFromPath : uses

    buildTreeFromFiles --> TFile : input
    buildTreeFromFiles --> TNode : output

    flattenTreeToFiles --> TNode : input
    flattenTreeToFiles --> TFile : output

    updateNode --> TNode : mutates
    deleteNode --> TNode : mutates
    addChildNode --> TNode : mutates
    findNodeByPath --> TNode : searches

    FileTreeBlockSpec --> BlockNoteEditorFT : updates block props
Loading

File-Level Changes

Change Details Files
Add a fully featured, configurable file tree BlockNote block with an editor-side config modal and viewer/layout components.
  • Define file tree types (files, nodes, component config, tree state) and language/color helpers.
  • Implement tree-building utilities (build, flatten, find/update/delete/add, ASCII serialization) plus default demo data.
  • Create React components for the interactive tree (TreeProvider, Tree, TreeNode) with keyboard navigation, ARIA roles, and visual affordances.
  • Add a FileViewer with simple code/markdown rendering and a basic resizable split-panel layout (ResizablePanelGroup/Panel/Handle).
  • Implement a BlockNote block spec (fileTreeBlockSpec) that renders the tree + viewer with style variants, lock toggle, and mobile/desktop layouts.
  • Add a configuration modal that lets users add/edit/delete files/folders, edit file content, and import/export JSON tree definitions.
apps/web/features/editor/slash-menu/file-tree/types.ts
apps/web/features/editor/slash-menu/file-tree/utils.ts
apps/web/features/editor/slash-menu/file-tree/components.tsx
apps/web/features/editor/slash-menu/file-tree/viewer.tsx
apps/web/features/editor/slash-menu/file-tree/block.tsx
apps/web/features/editor/slash-menu/file-tree/config-modal.tsx
apps/web/features/editor/slash-menu/file-tree/index.ts
Add a dedicated code block BlockNote block with language selection, filename metadata, syntax-highlighted rendering, and markdown-style ``` trigger.
  • Define language metadata, default filename derivation, code block props, and a CSS-variable-based syntax highlighting theme.
  • Implement a LanguageSelector dropdown with search, keyboard navigation, and language-specific icons.
  • Implement a CodeEditor textarea with tab insertion, auto-resizing, and keyboard handling.
  • Implement a CodeRenderer using react-syntax-highlighter and a custom theme, including copy-to-clipboard UI.
  • Define the codeBlockSpec BlockNote block with header (language selector + filename input), inline editing, and view/edit modes.
  • Add an input rule that listens for ``` + space and converts a paragraph into a codeBlock with default props.
  • Export the block and helpers from an index barrel and wire the spec into the editor schema and slash menu.
apps/web/features/editor/slash-menu/code-block/types.ts
apps/web/features/editor/slash-menu/code-block/language-icons.tsx
apps/web/features/editor/slash-menu/code-block/language-selector.tsx
apps/web/features/editor/slash-menu/code-block/editor.tsx
apps/web/features/editor/slash-menu/code-block/renderer.tsx
apps/web/features/editor/slash-menu/code-block/block.tsx
apps/web/features/editor/slash-menu/code-block/input-rule.tsx
apps/web/features/editor/slash-menu/code-block/index.ts
apps/web/features/editor/hooks/useEditorConfig.ts
apps/web/features/editor/hooks/useCustomSlashMenu.ts
apps/web/features/editor/components/editor-wrapper.tsx
apps/web/styles/globals.css
apps/web/package.json
Seed the app with demo content showcasing the new file tree block and expose a helper to construct fileTree blocks in preseed data.
  • Add fileTreeBlock helper for creating fileTree blocks with id, style, content, and default props.
  • Define FILE_TREE_DEMO_CONTENT note body that uses fileTreeBlock with a bundled JSON configuration and explanatory text.
  • Register a new "Interactive File Tree" note under the Documentation folder using the demo content.
apps/web/lib/preseed-data.ts
Update the service worker script (Serwist) bundle and pre-cache manifest to the latest generated version.
  • Replace the existing large sw.js bundle with a regenerated, minified version that preserves the same runtime caching strategies, GA integration, and offline fallback.
  • Refresh the precacheEntries manifest to point to the latest Next.js static asset chunk paths and hashes.
apps/web/public/sw.js

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 12, 2026

📝 Walkthrough

Walkthrough

Adds a new code-block feature (spec, editor, renderer, input-rule trigger, language tooling, slash-menu entry, styles) and replaces the previous single-file file-tree with a modular file-tree subsystem (block, components, viewer, utils, types, tests, demo data). Also updates editor inline-code UI to support selectable variants.

Changes

Cohort / File(s) Summary
Editor wrapper / inline-code UI
apps/web/features/editor/components/editor-wrapper.tsx
Registers backtick input-rule; adds inline-code variant picker UI, keyboard/pointer interactions, decorations, MutationObserver, and ARIA attributes; guarded by raw MDX mode.
Code Block: core
apps/web/features/editor/slash-menu/code-block/block.tsx, apps/web/features/editor/slash-menu/code-block/editor.tsx, apps/web/features/editor/slash-menu/code-block/renderer.tsx, apps/web/features/editor/slash-menu/code-block/types.ts, apps/web/features/editor/slash-menu/code-block/language-icons.tsx, apps/web/features/editor/slash-menu/code-block/language-selector.tsx
Adds codeBlockSpec, in-place CodeEditor, OverlayEditor renderer with copy UI, language config/types, language icons, selector component, detection and theme tokens.
Code Block: integration & trigger
apps/web/features/editor/hooks/useCustomSlashMenu.ts, apps/web/features/editor/hooks/useEditorConfig.ts, apps/web/features/editor/slash-menu/code-block/input-rule.tsx, apps/web/features/editor/slash-menu/code-block/index.ts
Adds "Code Block" slash-menu item, wires new codeBlockSpec into schema, exports code-block barrel, and implements registerCodeBlockTrigger input rule (``` + space → codeBlock).
File Tree: new modular implementation
apps/web/features/editor/slash-menu/file-tree/block.tsx, apps/web/features/editor/slash-menu/file-tree/components.tsx, apps/web/features/editor/slash-menu/file-tree/config-modal.tsx, apps/web/features/editor/slash-menu/file-tree/viewer.tsx, apps/web/features/editor/slash-menu/file-tree/index.ts, apps/web/features/editor/slash-menu/file-tree/types.ts, apps/web/features/editor/slash-menu/file-tree/utils.ts
Removes legacy monolith and introduces modular file-tree: a BlockNote block spec, TreeProvider/Tree UI, config modal editor, resizable viewer/panels, types, utils for build/serialize/parse, and public exports.
File Tree: tests & demo data
apps/web/features/editor/slash-menu/file-tree/__tests__/utils.test.ts, apps/web/lib/preseed-data.ts
Adds unit tests for file-tree utilities and a preseed demo note plus fileTreeBlock factory used in seed data.
Removed legacy file-tree
apps/web/features/editor/slash-menu/file-tree-block.tsx
Deletes the old monolithic file-tree implementation and its exported fileTreeBlockSpec.
Styling & dependencies
apps/web/styles/globals.css, apps/web/features/editor/styles/editor.css, apps/web/package.json, package.json
Adds syntax-highlight CSS variables, updates inline-code styles and picker UI, and adds react-syntax-highlighter (+ types) dependency; minor manifest formatting changes.

Sequence Diagram(s)

mermaid
sequenceDiagram
participant User
participant EditorDOM as Editor DOM
participant Trigger as registerCodeBlockTrigger
participant EditorAPI as BlockNoteEditor API
participant CodeBlock as CodeBlockSpec / OverlayEditor
Note over User,EditorDOM: User types "```" then Space
User->>EditorDOM: keydown/backtick events
EditorDOM->>Trigger: capture buffered backticks
Trigger->>EditorAPI: request replace current block with codeBlock (typescript, untitled.ts)
EditorAPI->>CodeBlock: insert/render new code block node
CodeBlock->>EditorDOM: focus code editor (autoFocus)

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested labels

codex

Poem

🐰 Three backticks tapped, a tiny hop so spry,
A block of code blooms under cursor sky.
Folders unfurl like carrot rows neatly,
Icons and snippets hum softly, sweetly—
Hop on, editor friend, let's code and fly. 🥕✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 6.78% 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 title accurately summarizes the main changes in the PR, which introduces two new major BlockNote components: an interactive file tree block and an enhanced code block component.

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

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch claude/merge-editor-file-tree-Mlqld

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.

Copy link
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

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

Hey - I've found 2 issues, and left some high level feedback:

  • In the file tree components, the indent guide spans in TreeNodeComponent are absolutely positioned but the container div isn’t position: relative, which may cause misaligned lines; consider adding relative to the row container or otherwise scoping those lines to the node row.
  • In the file tree and code block utilities/components there are a few unused or unfinished pieces (e.g. showJsonEditor/jsonContent state and the GripVertical import in config-modal.tsx, isLoading in viewer.tsx, and unused props like minSize, maxSize, collapsible in ResizablePanel); trimming or wiring these up will make the API clearer and reduce dead code.
  • In the new ResizablePanel/ResizableHandle implementation, the minSize/maxSize constraints are not enforced during resize (only an ad-hoc clamp in handleResize), which can lead to inconsistent behavior if panels are reused; consider centralising size clamping in the panel group or handle so the declared size constraints are always respected.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In the file tree components, the indent guide spans in `TreeNodeComponent` are absolutely positioned but the container div isn’t `position: relative`, which may cause misaligned lines; consider adding `relative` to the row container or otherwise scoping those lines to the node row.
- In the file tree and code block utilities/components there are a few unused or unfinished pieces (e.g. `showJsonEditor`/`jsonContent` state and the `GripVertical` import in `config-modal.tsx`, `isLoading` in `viewer.tsx`, and unused props like `minSize`, `maxSize`, `collapsible` in `ResizablePanel`); trimming or wiring these up will make the API clearer and reduce dead code.
- In the new `ResizablePanel`/`ResizableHandle` implementation, the `minSize`/`maxSize` constraints are not enforced during resize (only an ad-hoc clamp in `handleResize`), which can lead to inconsistent behavior if panels are reused; consider centralising size clamping in the panel group or handle so the declared size constraints are always respected.

## Individual Comments

### Comment 1
<location> `apps/web/features/editor/slash-menu/file-tree/block.tsx:272-276` </location>
<code_context>
+                            // Mobile: Stacked layout
+                            <div className="flex flex-col">
+                                <div className="max-h-[200px] overflow-auto p-2 border-b border-border">
+                                    <TreeProvider
+                                        showIndentLines={showIndentLines}
+                                        enableHoverHighlight={component.enableHoverHighlight}
+                                        onSelectFile={handleSelectFile}
+                                        initialState={{
+                                            expandedFolders: new Set(nodes.filter(n => n.type === 'folder').map(n => n.id)),
+                                            selectedFilePath: selectedNode?.path || null
</code_context>

<issue_to_address>
**issue (bug_risk):** `initialExpandedAll` and `expandAll` are not aligned with `TreeProvider`'s expanded state, so expansion and keyboard navigation can diverge.

`expandAll` mutates `node.isExpanded`, but `TreeNode` actually reads expansion from `TreeProvider.state.expandedFolders`. Because `initialState.expandedFolders` is built only from the root `nodes` array, nested folders are omitted and remain collapsed for keyboard navigation, even when `expandAll` has pre-expanded them. To keep expansion behavior consistent, either populate `expandedFolders` with all folder ids (including nested), or remove `expandAll` and drive expansion solely from `expandedFolders`.
</issue_to_address>

### Comment 2
<location> `apps/web/features/editor/slash-menu/file-tree/viewer.tsx:31-40` </location>
<code_context>
+    const [isLoading, setIsLoading] = useState(false)
</code_context>

<issue_to_address>
**suggestion:** `isLoading` is never updated, so the loading state and branch are effectively dead code.

Since `setIsLoading` is never called, the `Loading...` branch can never render and this state just adds noise. Please either remove the loading state/branch, or wire it into real async behavior (e.g., when integrating Shiki or another async highlighter) by toggling `isLoading` appropriately.

Suggested implementation:

```typescript
    const [highlightedCode, setHighlightedCode] = useState<string>('')
    const [copied, setCopied] = useState(false)

```

1. Remove any `isLoading` references in this component’s JSX, including the `Loading...` branch and any conditional rendering that depends on `isLoading`.
2. Remove any remaining imports or types related solely to the loading state, if present.
3. If you intend to add async highlighting later (e.g. with Shiki), reintroduce `isLoading` at that time and toggle it around the async call (set `true` before awaiting, `false` in `finally`).
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +272 to +276
<TreeProvider
showIndentLines={showIndentLines}
enableHoverHighlight={component.enableHoverHighlight}
onSelectFile={handleSelectFile}
initialState={{
Copy link
Contributor

Choose a reason for hiding this comment

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

issue (bug_risk): initialExpandedAll and expandAll are not aligned with TreeProvider's expanded state, so expansion and keyboard navigation can diverge.

expandAll mutates node.isExpanded, but TreeNode actually reads expansion from TreeProvider.state.expandedFolders. Because initialState.expandedFolders is built only from the root nodes array, nested folders are omitted and remain collapsed for keyboard navigation, even when expandAll has pre-expanded them. To keep expansion behavior consistent, either populate expandedFolders with all folder ids (including nested), or remove expandAll and drive expansion solely from expandedFolders.

Comment on lines +31 to +40
const [isLoading, setIsLoading] = useState(false)
const [copied, setCopied] = useState(false)

// Highlight code when selection changes
useEffect(() => {
if (!selectedNode || selectedNode.type === 'folder' || !selectedNode.content) {
setHighlightedCode('')
return
}

Copy link
Contributor

Choose a reason for hiding this comment

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

suggestion: isLoading is never updated, so the loading state and branch are effectively dead code.

Since setIsLoading is never called, the Loading... branch can never render and this state just adds noise. Please either remove the loading state/branch, or wire it into real async behavior (e.g., when integrating Shiki or another async highlighter) by toggling isLoading appropriately.

Suggested implementation:

    const [highlightedCode, setHighlightedCode] = useState<string>('')
    const [copied, setCopied] = useState(false)
  1. Remove any isLoading references in this component’s JSX, including the Loading... branch and any conditional rendering that depends on isLoading.
  2. Remove any remaining imports or types related solely to the loading state, if present.
  3. If you intend to add async highlighting later (e.g. with Shiki), reintroduce isLoading at that time and toggle it around the async call (set true before awaiting, false in finally).

Copy link
Contributor

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

Note

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

🤖 Fix all issues with AI agents
In `@apps/web/features/editor/hooks/useEditorConfig.ts`:
- Line 21: In useEditorConfig.ts update the schema entry so codeBlockSpec is
invoked like the other block factories: replace the direct reference codeBlock:
codeBlockSpec with a call to codeBlockSpec() so the factory function is executed
when building the editor config; this change should be made alongside the other
spec invocations in the same object literal to fix the TS2322 type error.

In `@apps/web/features/editor/slash-menu/code-block/input-rule.tsx`:
- Around line 36-41: The TypeScript error comes from accessing content[0].text
on a union type even after checking content[0].type === 'text'; update the check
to narrow the type before reading .text — e.g., after confirming
Array.isArray(content) && content.length === 1 && content[0].type === 'text',
assign content[0] to a typed local (or use a type assertion) such as const
textNode = content[0] as { type: 'text'; text: string } and then test
textNode.text === '```' before proceeding; locate and change the conditional
around the content array in input-rule.tsx to use this local/assertion so TS no
longer complains.

In `@apps/web/features/editor/slash-menu/code-block/renderer.tsx`:
- Line 10: Add the missing runtime and type packages for the imported
SyntaxHighlighter by adding "react-syntax-highlighter" and
"@types/react-syntax-highlighter" to the apps/web package.json dependencies (and
run yarn/npm install to update the lockfile); this will resolve the TS2307 error
referencing the import of PrismAsync as SyntaxHighlighter from
'react-syntax-highlighter'.

In `@apps/web/features/editor/slash-menu/code-block/types.ts`:
- Around line 61-128: The theme references missing CSS vars used by
codeBlockTheme (e.g., --sh-text, --sh-comment, --sh-keyword, --sh-string,
--sh-number, --sh-function, --sh-punctuation, --sh-operator, --sh-tag,
--sh-background, --sh-border); open your globals.css and add definitions for
these variables in :root for the light theme and in the .dark selector for the
dark theme, providing appropriate HSL (or hex) values for each variable so
syntax highlighting in codeBlockTheme can resolve colors and backgrounds
consistently across light/dark modes.

In `@apps/web/features/editor/slash-menu/file-tree/__tests__/utils.test.ts`:
- Around line 136-153: The test shows addChildNode isn't resulting in a tree
where the parent (found via findNodeById) contains the new child; update
addChildNode so it returns a new root tree with the child appended to the
correct parent node id (do not rely on the original srcNode reference), e.g.
traverse the tree produced by buildTreeFromFiles, locate the node matching the
provided parent id, and immutably insert the new TNode into that node's children
before returning the updated root; ensure findNodeById can locate nodes by id in
the returned tree and that node ids are preserved.
- Around line 171-188: The test's ASCII sample uses two "└──" roots which is
malformed and causes parseAsciiTree to return 3 nodes; update the test input in
utils.test.ts so the first root uses the branching glyph ("├── src/") and the
second uses the final glyph ("└── package.json") (or otherwise produce a
properly formed ASCII tree), and keep the expectation that parseAsciiTree(...)
yields two root nodes; this change targets the test around parseAsciiTree in the
file-tree utils tests so no parser changes are required.

In `@apps/web/features/editor/slash-menu/file-tree/block.tsx`:
- Line 210: The floating action buttons wrapper in block.tsx (the div with
className "absolute top-2 right-2 z-20 flex items-center gap-1 opacity-0
group-hover:opacity-100 transition-opacity") is invisible to keyboard users;
update that wrapper's className to also include a focus/focus-within rule (e.g.
add "group-focus-within:opacity-100" or "focus-within:opacity-100") so the
controls become visible when any child receives keyboard focus, and verify the
contained buttons are keyboard-focusable and have appropriate accessible names
(aria-labels) so they can be discovered and operated by keyboard users.

In `@apps/web/features/editor/slash-menu/file-tree/components.tsx`:
- Around line 295-306: The nested TreeNodeComponent is given a no-op onFocus
handler which prevents focus updates for children; instead propagate the real
focus handler from the parent (thread the onFocus prop down) or centralize focus
management in the Tree using visibleNodes/focusedIndex. Locate the
TreeNodeComponent invocation that sets onFocus={() => { }} and replace it by
forwarding the parent onFocus (e.g., onFocus={onFocus}) or wire it to the Tree's
focus setter so child nodes call the same handler that updates
focusedPath/focusedIndex; ensure focusedPath and focusedIndex logic in Tree
consumes these events so keyboard navigation and highlight work for nested
items.
- Around line 125-155: The Tree component's handleKeyDown currently swallows
Enter/Space/ArrowRight/ArrowLeft but focus stays on the tree container so those
keys never reach TreeNodeComponent; update Tree's handleKeyDown to handle these
keys by reading the focused node via visibleNodes[focusedIndex] and calling the
tree context actions: destructure onToggleExpand, onSelectFile, and state from
useTreeContext() in the Tree component, then for Enter/Space call
onSelectFile(focusedNode) (or the appropriate select handler), for ArrowRight
call onToggleExpand(focusedNode.id, true) (or open if collapsed), for ArrowLeft
call onToggleExpand(focusedNode.id, false) (or collapse if expanded), and
preventDefault as needed so keyboard selection/expansion works while focus
remains on the container.
- Around line 244-256: The indent line spans (rendered when showIndentLines &&
depth > 0) are absolutely positioned but their container lacks positioning;
update the parent row container (the div that wraps the spans and the row
contents) to have relative positioning (e.g., add className "relative" or
style={{ position: 'relative' }}) so the left/top/bottom offsets on the span
elements (from the Array.map) are calculated against that row; ensure you modify
the same parent that contains the spans (the tree row wrapper) rather than a
higher ancestor.

In `@apps/web/features/editor/slash-menu/file-tree/config-modal.tsx`:
- Around line 353-358: handleSaveEdit currently only updates the node.name so
node.path and descendants' paths remain stale; compute the new path when
renaming (using node.id and editValue.trim()) and update the node's path and all
descendant paths before calling onUpdate. Modify handleSaveEdit to derive
newPath (and a recursive update of children paths or call the existing
path-recompute utility used by flattenTreeToFiles), then call onUpdate(node.id,
{ name: newName, path: newPath, children: updatedChildren }) and finally
setIsEditing(false).
- Around line 165-171: The modal in config-modal.tsx currently renders a plain
div with role="dialog" but lacks focus trapping, Escape-to-close, and
backdrop-click-to-close behavior; update the component to use an accessible
dialog provider (e.g., Radix Dialog or your app's headless dialog component) or
implement a small hook: add a focus-trap (or use a focusTrap hook), use
useEffect to listen for Escape and call the modal close handler, and attach an
onClick on the backdrop container that closes when clicking outside the dialog
content (ensure clicks inside content stopPropagation); also remove the
arbitrary Tailwind class z-[100] and replace it with the project's tokenized
z-index (e.g., z-modal or the appropriate design token) to conform to styling
guidelines.
- Around line 81-92: handleAddChild currently initializes new nodes with path:
'' which causes blank TFile paths when flattened; update handleAddChild to
compute the child's path from its parent by importing and using findNodeById
from ./utils to lookup the parent node (use parent.path and append newNode.name
with a '/' separator when parent.path is non-empty), set newNode.path to that
derived value before calling setNodes, and keep existing fields (id, name, type,
children/content, isExpanded) unchanged so flattenTreeToFiles and
buildTreeFromFiles receive correct paths.

In `@apps/web/features/editor/slash-menu/file-tree/utils.ts`:
- Around line 110-156: The folder-detection logic in parseAsciiTree is
incorrectly using line.includes('├──') and line.includes('└──'), causing most
indented entries to be classified as folders; change isFolder (in
parseAsciiTree) to rely only on the trailing slash convention used by
serializeTreeToAscii (i.e., keep name.endsWith('/')) and remove the checks for
the box-drawing connectors so only names that end with '/' become type 'folder'
(ensure children is only set for folders and files keep children undefined).

In `@apps/web/features/editor/slash-menu/file-tree/viewer.tsx`:
- Around line 232-252: ResizablePanel's internal state (size via useState)
freezes the initial defaultSize prop so updates from the parent never take
effect; remove the internal state and read the prop directly: stop using
useState(defaultSize)/setSize, compute actualSize = collapsed ? 0 : defaultSize
(or the incoming leftPanelSize) and keep the rest (className, collapsible,
collapsed) unchanged so the panel reacts to prop updates; alternatively, if you
need internal state, add a useEffect that calls setSize(defaultSize) when
defaultSize changes. Ensure you reference the ResizablePanel function, the
defaultSize prop, the size/setSize state, and actualSize when making the change.
- Around line 259-297: ResizableHandle is focusable (tabIndex=0) and announced
as role="separator" but lacks keyboard handlers, so add an onKeyDown on the root
div to support ArrowLeft/ArrowRight (and optionally ArrowUp/ArrowDown if
relevant) to call the onResize prop with a small delta (e.g., ±10) and
preventDefault; use the existing onResize function and isDragging/handleRef
state to keep behavior consistent, and optionally support modifiers (Shift for
larger step) and call setIsDragging(false) only on mouse up as before. Ensure
the handler references the ResizableHandle component's onResize prop and updates
focus behavior without changing existing mouse logic.
🟡 Minor comments (5)
apps/web/features/editor/slash-menu/code-block/renderer.tsx-37-50 (1)

37-50: ⚠️ Potential issue | 🟡 Minor

Minor: setTimeout on line 44 is not cleaned up on unmount.

If the component unmounts within the 2-second window, setIsCopied(false) fires on an unmounted component. Consider storing the timeout ID in a ref and clearing it in a cleanup path, or use an AbortController/useEffect pattern.

apps/web/features/editor/slash-menu/code-block/block.tsx-62-84 (1)

62-84: ⚠️ Potential issue | 🟡 Minor

Dead variable: currentExt is never used.

currentExt on line 65 is computed but never referenced. Looks like a leftover from a refactor.

🧹 Remove unused variable
             const handleLanguageChange = useCallback(
                 (newLanguage: string) => {
                     // Auto-update filename extension if it's the default name
-                    const currentExt = localFileName.split('.').pop()
                     const defaultName = getDefaultFilename(language)
                     const isDefaultName = localFileName === defaultName || localFileName.startsWith('untitled.')
apps/web/features/editor/slash-menu/file-tree/viewer.tsx-30-31 (1)

30-31: ⚠️ Potential issue | 🟡 Minor

isLoading is never set to true — the loading branch is dead code.

isLoading is initialized to false and never updated. The conditional at Line 86 (isLoading ? <Loading/> : ...) never takes the loading branch. Either remove the loading state or wire it up to an actual async operation (e.g., when Shiki highlighting is added).

apps/web/features/editor/slash-menu/file-tree/viewer.tsx-3-6 (1)

3-6: ⚠️ Potential issue | 🟡 Minor

Doc comment claims Shiki is used, but it isn't.

The module-level comment says "Uses Shiki for code highlighting" — the actual implementation is a plain text display stub. Update the comment to reflect reality.

apps/web/features/editor/slash-menu/file-tree/config-modal.tsx-142-161 (1)

142-161: ⚠️ Potential issue | 🟡 Minor

Imported JSON is cast without structural validation.

JSON.parse(text) as TComponent blindly trusts the imported file's shape. If data.files is undefined or not an array, buildTreeFromFiles will throw at runtime. Consider adding a basic guard.

Suggested fix
                 const data = JSON.parse(text) as TComponent
+                if (!data.name || !Array.isArray(data.files)) {
+                    console.error('Invalid file tree JSON format')
+                    return
+                }
                 setComponentName(data.name)
🧹 Nitpick comments (10)
apps/web/features/editor/slash-menu/file-tree/__tests__/utils.test.ts (1)

65-78: Sorting test may be fragile — depends on implementation detail.

The comment on line 66 acknowledges the sort order is unclear. If buildTreeFromFiles changes its sort strategy (e.g., folders-first), this test will break. Consider either documenting the intended sort contract in the utility or testing only for presence rather than exact ordering.

apps/web/features/editor/slash-menu/code-block/input-rule.tsx (2)

79-87: Stub hook useCodeBlockTrigger has no implementation.

This exported function is a no-op placeholder that doesn't use React hooks despite the use prefix (violating the Rules of Hooks convention for naming). Either implement it with useEffect or remove it to avoid confusion. The AI summary confirms it's consumed via the imperative registerCodeBlockTrigger instead.


23-65: Buffer can drift out of sync with actual editor content.

The keystroke buffer tracks raw key events but doesn't account for cursor repositioning (mouse clicks, arrow keys, undo). This is mitigated by the content check on lines 36–41 (verifying the block text is exactly ```), so it won't produce false positives. Acceptable as-is, but worth a brief comment noting the safety net.

apps/web/features/editor/slash-menu/code-block/language-selector.tsx (1)

152-158: Dropdown may be clipped by overflow: hidden ancestors.

Since this renders inline (no portal), the dropdown could be cut off if a parent container has overflow: hidden or overflow: auto. If the code block header lives inside a scrollable editor region, consider rendering the dropdown via a portal or verifying it isn't clipped in practice.

apps/web/features/editor/slash-menu/code-block/types.ts (1)

61-93: Duplicated style properties between code and pre selectors.

Lines 62–76 and 77–93 share nearly identical properties (color, background, fontFamily, textAlign, whiteSpace, wordSpacing, wordBreak, overflowWrap, lineHeight, fontSize, tabSize, hyphens). Extract the shared base to reduce maintenance burden.

♻️ Suggested extraction
+const sharedCodeStyles: CSSProperties = {
+    color: 'hsl(var(--sh-text))',
+    background: 'none',
+    fontFamily:
+        'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace',
+    textAlign: 'left',
+    whiteSpace: 'pre-wrap',
+    wordSpacing: 'normal',
+    wordBreak: 'normal',
+    overflowWrap: 'anywhere',
+    lineHeight: '1.6',
+    fontSize: '14px',
+    tabSize: 2,
+    hyphens: 'none'
+}
+
 export const codeBlockTheme: TCustomTheme = {
-    'code[class*="language-"]': {
-        color: 'hsl(var(--sh-text))',
-        background: 'none',
-        fontFamily: ...,
-        ...
-    },
-    'pre[class*="language-"]': {
-        color: 'hsl(var(--sh-text))',
-        background: 'none',
-        fontFamily: ...,
-        ...
-    },
+    'code[class*="language-"]': { ...sharedCodeStyles },
+    'pre[class*="language-"]': {
+        ...sharedCodeStyles,
+        padding: '1.25rem',
+        margin: '0'
+    },
     comment: { ... },
apps/web/features/editor/slash-menu/code-block/language-icons.tsx (1)

69-73: Consider using cn() for class merging consistency.

Template literal concatenation can produce extra spaces when iconClass or className is empty. The cn utility from @skriuw/shared is already used throughout the codebase and handles this cleanly.

♻️ Suggested change
+import { cn } from '@skriuw/shared'
+
 export function LanguageIcon({ language, size = 16, className = '' }: LanguageIconProps) {
     const IconComponent = getLanguageIcon(language)
     const iconClass = getLanguageIconClass(language)
 
-    return <IconComponent size={size} className={`flex-shrink-0 ${iconClass} ${className}`} />
+    return <IconComponent size={size} className={cn('flex-shrink-0', iconClass, className)} />
 }
apps/web/features/editor/slash-menu/code-block/block.tsx (1)

86-101: setTimeout blur check is fragile — consider relatedTarget.

The 10ms delay to check document.activeElement is a known workaround but can be unreliable across browsers and input methods. The FocusEvent.relatedTarget property gives you the element receiving focus without a timeout and is supported in all modern browsers.

♻️ Alternative using relatedTarget
-            const handleBlur = useCallback(() => {
-                // Small delay to check if focus moved within our container
-                setTimeout(() => {
-                    if (containerRef.current && !containerRef.current.contains(document.activeElement)) {
-                        setIsEditing(false)
-                        editor.updateBlock(block.id, {
-                            props: {
-                                language,
-                                fileName: localFileName,
-                                code: localCode
-                            }
-                        })
-                    }
-                }, 10)
-            }, [editor, block.id, language, localFileName, localCode])
+            const handleBlur = useCallback((e: React.FocusEvent) => {
+                // Check if focus is moving to another element inside the container
+                if (containerRef.current && !containerRef.current.contains(e.relatedTarget as Node)) {
+                    setIsEditing(false)
+                    editor.updateBlock(block.id, {
+                        props: {
+                            language,
+                            fileName: localFileName,
+                            code: localCode
+                        }
+                    })
+                }
+            }, [editor, block.id, language, localFileName, localCode])

Note: The CodeEditor's onBlur prop type would need to accept React.FocusEvent accordingly.

apps/web/features/editor/slash-menu/file-tree/block.tsx (2)

113-136: Redundant tree build on mount: useState initializer and useEffect both construct the tree.

The useState initializer (Line 113) builds the tree once on mount. The useEffect (Line 133) runs immediately after mount with the same inputs, rebuilding the tree and calling setNodes again. Additionally, after handleSaveConfig (Line 159) calls setNodes, the useEffect will fire again (because component.files changes via contentStringcomponent memo), overwriting the result — potentially with a different outcome if initialExpandedAll is true (the effect applies expandAll, but handleSaveConfig does not).

Consider removing the useState initializer logic and relying solely on the useEffect, or removing the effect and using a key or ref-based approach to handle external content changes.

Suggested fix: remove the redundant initializer
-            const [nodes, setNodes] = useState<TNode[]>(() => {
-                const tree = buildTreeFromFiles(component.files)
-                // Auto-expand all if configured
-                if (block.props.initialExpandedAll) {
-                    return expandAll(tree)
-                }
-                return tree
-            })
+            const [nodes, setNodes] = useState<TNode[]>([])

The useEffect at Line 133 already handles the initial build and subsequent syncs.


183-193: Nested setIsCollapsed inside setLeftPanelSize updater.

Calling setIsCollapsed inside the setLeftPanelSize functional updater is a side-effect within a state updater — it works in React but is fragile and can cause unexpected render ordering. Extract the collapse logic outside the updater.

Suggested refactor
             const handleResize = useCallback((delta: number) => {
-                setLeftPanelSize((prev) => {
-                    const newSize = prev + (delta / 10)
-                    if (newSize < 15) {
-                        setIsCollapsed(true)
-                        return 0
-                    }
-                    setIsCollapsed(false)
-                    return Math.max(20, Math.min(70, newSize))
-                })
+                setLeftPanelSize((prev) => {
+                    const newSize = prev + (delta / 10)
+                    return newSize < 15 ? 0 : Math.max(20, Math.min(70, newSize))
+                })
+                setLeftPanelSize((current) => {
+                    setIsCollapsed(current === 0)
+                    return current
+                })
             }, [])

Or better yet, derive isCollapsed from leftPanelSize directly:

-            const [isCollapsed, setIsCollapsed] = useState(false)
+            const isCollapsed = leftPanelSize === 0
apps/web/features/editor/slash-menu/file-tree/utils.ts (1)

205-215: updateNode always recurses into children even after finding the target.

When the node with matching id is found, the function returns the updated node but continues to map over siblings and their children. Since map processes every element, this is O(n) regardless. More importantly, if the updated node itself has children, those children are not checked — the spread { ...node, ...updates } replaces the whole node. This is fine for shallow updates but worth noting: if updates includes a children key, it will replace (not merge) the children array. This is likely the intended behavior but could surprise callers.

Comment on lines +136 to +153
it('should add child node', () => {
const tree = buildTreeFromFiles(sampleFiles)
const srcNode = tree.find(n => n.name === 'src')
if (!srcNode) throw new Error('src node not found')

const newFile: TNode = {
id: 'new-id',
name: 'utils.ts',
type: 'file',
path: 'src/utils.ts',
content: ''
}

const updatedTree = addChildNode(tree, srcNode.id, newFile)
const updatedSrc = findNodeById(updatedTree, srcNode.id)

expect(updatedSrc?.children?.find(n => n.name === 'utils.ts')).toBeDefined()
})
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

addChildNode test fails: child not found after insertion.

CI confirms updatedSrc?.children?.find(n => n.name === 'utils.ts') is undefined. The addChildNode utility either isn't appending the child to the correct parent, or it returns a new tree where the parent reference has changed and findNodeById can't locate it.

🧰 Tools
🪛 GitHub Check: Lint and Format

[failure] 152-152: error: expect(received).toBeDefined()
Received: undefined

  at <anonymous> (/home/runner/work/skriuw/skriuw/apps/web/features/editor/slash-menu/file-tree/__tests__/utils.test.ts:152:69)
🤖 Prompt for AI Agents
In `@apps/web/features/editor/slash-menu/file-tree/__tests__/utils.test.ts` around
lines 136 - 153, The test shows addChildNode isn't resulting in a tree where the
parent (found via findNodeById) contains the new child; update addChildNode so
it returns a new root tree with the child appended to the correct parent node id
(do not rely on the original srcNode reference), e.g. traverse the tree produced
by buildTreeFromFiles, locate the node matching the provided parent id, and
immutably insert the new TNode into that node's children before returning the
updated root; ensure findNodeById can locate nodes by id in the returned tree
and that node ids are preserved.

@remcostoeten
Copy link
Owner Author

@sourcery-ai @coderabbitai re-review i fixed all

@sourcery-ai
Copy link
Contributor

sourcery-ai bot commented Feb 12, 2026

Sure! I'm generating a new review now.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 12, 2026

@remcostoeten I'll review the changes now that you've addressed the issues.

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

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

Hey - I've found 1 issue, and left some high level feedback:

  • In the file tree ConfigModal handleImportJson, invalid or malformed JSON currently only logs to the console and silently leaves the UI unchanged; consider adding user-facing error feedback and stricter schema validation before applying the imported structure so users understand why their import failed.
  • The code block markdown trigger in registerCodeBlockTrigger uses a simple global buffer tied to container keydown events; you may want to reset this buffer on focus/blur changes or when the current block changes to avoid unexpected ``` + space conversions after navigation or when typing across multiple blocks.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In the file tree ConfigModal `handleImportJson`, invalid or malformed JSON currently only logs to the console and silently leaves the UI unchanged; consider adding user-facing error feedback and stricter schema validation before applying the imported structure so users understand why their import failed.
- The code block markdown trigger in `registerCodeBlockTrigger` uses a simple global `buffer` tied to container keydown events; you may want to reset this buffer on focus/blur changes or when the current block changes to avoid unexpected ``` + space conversions after navigation or when typing across multiple blocks.

## Individual Comments

### Comment 1
<location> `apps/web/features/editor/slash-menu/file-tree/viewer.tsx:88-58` </location>
<code_context>
+            </div>
+
+            {/* Copy confirmation toast */}
+            {copied && (
+                <div className="absolute bottom-4 right-4 flex items-center gap-2 px-3 py-2 bg-green-500/20 border border-green-500/30 rounded-md text-green-400 text-sm animate-in fade-in slide-in-from-bottom-2">
+                    <Check className="w-4 h-4" />
+                    Copied to clipboard
+                </div>
+            )}
+        </div>
</code_context>

<issue_to_address>
**nitpick (bug_risk):** The copy toast is absolutely positioned but the parent container isn’t positioned, so it may attach to an unexpected ancestor.

Because the toast is `absolute` (`bottom-4 right-4`), it will position relative to the nearest `position: relative` ancestor, which may be outside `FileViewer` or be the document body, depending on layout. To ensure it stays anchored to the viewer, add `relative` to the top-level container:

```tsx
<div className={cn('relative flex flex-col h-full', className)}>
  ...
</div>
```
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

return (
<div className={cn('flex items-center justify-center h-full text-muted-foreground', className)}>
<p className="text-sm">Select a file to view its contents</p>
</div>
Copy link
Contributor

Choose a reason for hiding this comment

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

nitpick (bug_risk): The copy toast is absolutely positioned but the parent container isn’t positioned, so it may attach to an unexpected ancestor.

Because the toast is absolute (bottom-4 right-4), it will position relative to the nearest position: relative ancestor, which may be outside FileViewer or be the document body, depending on layout. To ensure it stays anchored to the viewer, add relative to the top-level container:

<div className={cn('relative flex flex-col h-full', className)}>
  ...
</div>

@sourcery-ai
Copy link
Contributor

sourcery-ai bot commented Feb 12, 2026

Hey @remcostoeten, I've posted a new review for you!

Copy link
Contributor

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

🤖 Fix all issues with AI agents
In `@apps/web/features/editor/slash-menu/code-block/block.tsx`:
- Around line 137-149: handleContainerKeyDown currently removes the entire block
on a single Delete/Backspace press when the containerRef is focused by calling
editor.removeBlocks([block.id]); change this to guard against accidental
deletion by first checking the block's editable content (e.g., localCode) and
only remove if it's empty or require a double-press/confirmation: inside
handleContainerKeyDown (and where containerRef is used) read localCode (or the
block's content) and if non-empty, prevent removal and optionally prompt or
require a second keypress within a short timeout before calling
editor.removeBlocks([block.id]); ensure you reference containerRef,
handleContainerKeyDown, localCode, editor.removeBlocks, and block.id when
implementing the guard or confirmation flow.
- Around line 52-59: The two useEffect hooks that call setLocalCode(code) and
setLocalFileName(fileName) should not overwrite in-progress edits; modify them
to only sync when the editor is not active by adding isEditing to the dependency
arrays and gating the update with a check (e.g., if (!isEditing)
setLocalCode(code) and if (!isEditing) setLocalFileName(fileName)); update the
effects that reference code, fileName, setLocalCode, setLocalFileName and
isEditing accordingly so external prop changes only apply when isEditing is
false.

In `@apps/web/features/editor/slash-menu/file-tree/components.tsx`:
- Around line 183-194: The current focus callback captures the top-level node
index causing nested clicks to set focus to the ancestor; change the onFocus
prop on Tree and TreeNode to accept a path (string) instead of being a no-arg
callback, update the TreeNodeProps/TreeNodeComponent signature accordingly, and
when rendering children call onFocus(child.path); in Tree use
setFocusedIndex(visibleNodes.findIndex(n => n.path === path)) and compute
focusedPath from visibleNodes[focusedIndex]?.path so the clicked child resolves
its own index (update all usages around TreeNode render and the descendant
render sites that currently pass onFocus unchanged).

In `@apps/web/features/editor/slash-menu/file-tree/viewer.tsx`:
- Around line 73-95: The copy-toast is absolutely positioned but its nearest
container (<div className={cn('flex flex-col h-full', className)}>) has no
positioning, so the toast can float outside the viewer; update the root
container used by Viewer (the div wrapping FileHeader/Content/Copy confirmation)
to be a positioned ancestor (e.g., add relative positioning via the class list
for the root div) so the absolute toast (rendered when copied is true) is
positioned correctly relative to the viewer; verify the change in the component
that renders FileHeader, MarkdownViewer, CodeViewer and the copied state.

In `@apps/web/package.json`:
- Line 90: The file apps/web/package.json is missing a trailing newline (the
file ends immediately after the final closing brace "}") — add a single newline
character at end-of-file so the file ends with a newline to satisfy
POSIX/editor/linter expectations.
- Line 64: The dependency "react-syntax-highlighter" was bumped to 16.1.0 but
its compatibility with React 19/Next.js is unverified; run targeted tests of the
new CodeBlock component and pages that render highlighted code under React 19
(unit tests, Storybook, and an e2e run in the Next.js dev/build server) and if
you observe issues either pin the package to the last known compatible version
or add a compatibility shim/workaround; update package.json and lockfile
accordingly and include a short comment in package.json or the PR describing the
chosen remediation.

In `@apps/web/styles/globals.css`:
- Around line 84-95: Stylelint flags blank lines before custom properties;
remove the empty line before the first custom property so the properties are
grouped consistently (e.g., delete the blank line immediately before --sh-text
and do the same in the .dark block before its first --sh-... property), ensuring
the block starts directly with the custom-property declarations like --sh-text,
--sh-comment, --sh-keyword, etc., and then run Stylelint to confirm the
custom-property-empty-line-before errors are resolved.
🧹 Nitpick comments (12)
apps/web/features/editor/slash-menu/code-block/block.tsx (3)

50-50: Unused ref: languageSelectorRef is declared but never read.

It's assigned to a wrapper <div> on Line 176 but nothing in the component references it. Remove it to avoid dead code.

♻️ Suggested cleanup
-            const languageSelectorRef = useRef<HTMLDivElement>(null)

And on Line 176:

-                        <div ref={languageSelectorRef}>
+                        <div>

151-166: Add an aria-label or role to the focusable container.

The outer <div> has tabIndex={-1} and a keyDown handler (Delete removes the block), but no accessible name or role. Screen readers will announce it as a generic container. Adding context helps:

 <div
     ref={containerRef}
     className={cn(...)}
     tabIndex={-1}
     onKeyDown={handleContainerKeyDown}
     onBlurCapture={handleBlur}
     data-block-type="codeBlock"
+    role="region"
+    aria-label={`Code block: ${localFileName}`}
 >

As per coding guidelines, "Maintain accessible interactions: focus visibility, keyboard navigation, labels".


154-161: Multiple arbitrary Tailwind values — consider defining theme tokens.

Lines 158, 171–172 use bg-[hsl(var(--sh-background))]/90 and border-[hsl(var(--sh-border))]. With Tailwind v4's CSS-driven config, these custom properties can be registered as theme tokens (e.g., via @theme in your CSS), letting you write bg-sh-background/90 and border-sh-border instead of arbitrary bracket notation.

As per coding guidelines, "Avoid arbitrary values in Tailwind unless there is no tokenized alternative" and "use CSS-driven Tailwind v4 (@theme, CSS vars), not a required tailwind.config.ts".

apps/web/styles/globals.css (1)

85-95: Consider registering --sh-* variables in the @theme block if they need Tailwind utility access.

The existing design tokens (e.g., --border, --muted) are mapped in the @theme block (Lines 11–46) so Tailwind can generate utility classes. The new --sh-* variables aren't registered there. If these will only be consumed in inline styles or component-level CSS this is fine, but if you plan to use them as Tailwind classes (e.g., text-sh-text, bg-sh-background), you'll need corresponding @theme entries like --color-sh-text: hsl(var(--sh-text)).

Also applies to: 131-141

apps/web/features/editor/slash-menu/file-tree/utils.ts (1)

110-156: parseAsciiTree depth calculation is fragile and the path reconstruction can produce incorrect results.

Two issues:

  1. Depth via count (Line 121): Counting characters works for typical indentation but breaks for root-level entries that start with ├── or └── (no → depth 0 for all root entries is correct), however if the ASCII tree has entries at varying depths without guides (e.g., minimally indented trees), the depth will be wrong.

  2. Path reconstruction (Lines 142–148): findParentNode walks the entire root tree looking for a node whose children array matches parent.node by reference. At depth > 1, parent.node is newNode.children! of a just-created node — this is correct. But the path is set only to parentNode.path/cleanName; for deeper nesting, you rely on findParentNode returning the immediate parent, which it does. So this part works but is O(n) per node insertion, making the overall parse O(n²).

For round-trip fidelity, consider adding an integration test that serializes → parses → serializes and asserts the output matches.

apps/web/features/editor/slash-menu/file-tree/components.tsx (1)

228-242: handleKeyDown on TreeNodeComponent is redundant but harmless.

Since the Tree container (tabIndex={0}) captures all keyboard events and the inner div has tabIndex={-1} (won't receive focus via keyboard navigation), this onKeyDown handler is effectively dead code. It won't cause issues, but could be removed to reduce confusion.

apps/web/features/editor/slash-menu/file-tree/config-modal.tsx (2)

78-94: handleAddChild reads nodes from closure — could be stale under rapid calls.

findNodeById(nodes, parentId) on Line 79 reads from the nodes captured by the useCallback dependency. If two handleAddChild calls fire before a re-render, the second call sees stale state. This is unlikely in practice (user-initiated actions) but could be avoided by performing the lookup inside the setNodes updater.

Sketch
 const handleAddChild = useCallback((parentId: string, type: 'file' | 'folder') => {
-    const parent = findNodeById(nodes, parentId)
-    const parentPath = parent?.path || ''
     const childName = type === 'folder' ? 'New Folder' : 'new-file.ts'
-    const childPath = parentPath ? `${parentPath}/${childName}` : childName
-    ...
-    setNodes((prev) => addChildNode(prev, parentId, newNode))
-}, [nodes])
+    setNodes((prev) => {
+        const parent = findNodeById(prev, parentId)
+        const parentPath = parent?.path || ''
+        const childPath = parentPath ? `${parentPath}/${childName}` : childName
+        const newNode: TNode = { ... path: childPath, ... }
+        return addChildNode(prev, parentId, newNode)
+    })
+}, [])

148-168: Import JSON validation is minimal — malformed files entries won't be caught.

Line 156 checks that data.name is a string and data.files is an array, but doesn't validate individual file entries (e.g., each entry having a path string). buildTreeFromFiles on Line 163 could behave unexpectedly if entries lack path.

apps/web/features/editor/slash-menu/file-tree/viewer.tsx (2)

212-238: minSize and maxSize props are declared in the type but never used.

ResizablePanelProps defines minSize and maxSize (Lines 215–216) but they aren't destructured or applied. Either implement clamping logic or remove the unused props to avoid misleading the consumer.


24-40: highlightedCode state is effectively a pass-through of selectedNode.content.

The useEffect (Lines 29–40) sets highlightedCode to selectedNode.content without transformation. The comment indicates future Shiki integration, so this scaffolding makes sense. Just noting that language (Line 35) is computed but unused within the effect.

apps/web/features/editor/slash-menu/file-tree/block.tsx (2)

256-267: initialState creates new objects on every render — minor waste.

new Set(collectAllFolderIds(nodes)) in the initialState prop produces a fresh Set each render. Since TreeProvider only reads initialState on mount (via useState), this doesn't cause bugs, but it creates garbage. Wrap in useMemo if you want to avoid unnecessary allocations:

+const initialExpandedSet = useMemo(() => new Set(collectAllFolderIds(nodes)), [nodes])
 ...
 <TreeProvider
     initialState={{
-        expandedFolders: new Set(collectAllFolderIds(nodes)),
+        expandedFolders: initialExpandedSet,
         selectedFilePath: selectedNode?.path || null
     }}
 >

Also applies to: 283-295


22-55: useIsMobile and usePrefersReducedMotion could be extracted to a shared hooks module.

usePrefersReducedMotion here overlaps with the standalone prefersReducedMotion() function in components.tsx (Line 27). Consider extracting both hooks to a shared location (e.g., @/features/editor/hooks or a shared utils module) to avoid duplication as the feature grows.

Comment on lines +183 to +194
{nodes.map((node, index) => (
<TreeNode
key={node.id}
node={node}
depth={0}
focusedPath={visibleNodes[focusedIndex]?.path}
onFocus={() => setFocusedIndex(visibleNodes.findIndex((n) => n.path === node.path))}
/>
))}
</div>
)
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Focus tracking is incorrect for nested tree items.

Each top-level node in Tree captures its own index in visibleNodes via onFocus (Line 189). This same callback is passed unchanged to all descendants (Line 320). When a user clicks a nested item (e.g., src/index.ts), the onFocus fires for the top-level ancestor (src), so focusedIndex and focusedPath point to the wrong node. The focus ring (Line 252) then highlights the parent instead of the clicked child.

Fix: pass the child node's identity into onFocus so it can resolve the correct index.

Proposed fix

In Tree, replace the per-node onFocus with a callback that accepts a path:

+    const handleFocusNode = useCallback(
+        (path: string) => {
+            const idx = visibleNodes.findIndex((n) => n.path === path)
+            if (idx >= 0) setFocusedIndex(idx)
+        },
+        [visibleNodes]
+    )
+
     return (
         <div ref={treeRef} role="tree" ...>
             {nodes.map((node) => (
                 <TreeNode
                     key={node.id}
                     node={node}
                     depth={0}
                     focusedPath={visibleNodes[focusedIndex]?.path}
-                    onFocus={() => setFocusedIndex(visibleNodes.findIndex((n) => n.path === node.path))}
+                    onFocus={handleFocusNode}
                 />
             ))}
         </div>
     )

Then update TreeNodeProps and TreeNodeComponent:

 type TreeNodeProps = {
     node: TNode
     depth: number
     focusedPath?: string
-    onFocus: () => void
+    onFocus: (path: string) => void
 }

 // In TreeNodeComponent:
-                onFocus={onFocus}
+                onFocus={() => onFocus(node.path)}
                 tabIndex={-1}

And in child rendering:

                     <TreeNodeComponent
                         key={child.id}
                         node={child}
                         depth={depth + 1}
                         focusedPath={focusedPath}
-                        onFocus={onFocus}
+                        onFocus={onFocus}
                     />

Children will call onFocus(child.path) which correctly resolves their own index.

Also applies to: 312-324

🤖 Prompt for AI Agents
In `@apps/web/features/editor/slash-menu/file-tree/components.tsx` around lines
183 - 194, The current focus callback captures the top-level node index causing
nested clicks to set focus to the ancestor; change the onFocus prop on Tree and
TreeNode to accept a path (string) instead of being a no-arg callback, update
the TreeNodeProps/TreeNodeComponent signature accordingly, and when rendering
children call onFocus(child.path); in Tree use
setFocusedIndex(visibleNodes.findIndex(n => n.path === path)) and compute
focusedPath from visibleNodes[focusedIndex]?.path so the clicked child resolves
its own index (update all usages around TreeNode render and the descendant
render sites that currently pass onFocus unchanged).

"react-dom": "19.2.0",
"react-hook-form": "^7.67.0",
"react-simple-code-editor": "^0.14.1",
"react-syntax-highlighter": "^16.1.0",
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🌐 Web query:

react-syntax-highlighter latest version 2025 2026

💡 Result:

As of February 12, 2026, the latest npm version of react-syntax-highlighter is 16.1.0 (released Oct 28, 2025). [1][2][3]

Notes for 2025–2026:

  • 16.0.0 (Oct 22, 2025) was a major release that upgraded refractor to v5 and may introduce dependency-breaking changes. [4]
  • No newer (2026) release is shown in the “latest” feeds/tools above as of today. [1][2][3]

To verify from your terminal:

npm view react-syntax-highlighter version
npm view react-syntax-highlighter time --json

Sources: Snyk package page/version list [1][2], jsDelivr package page [3], NewReleases.io release notes [4].


🌐 Web query:

react-syntax-highlighter React 19 compatibility

💡 Result:

react-syntax-highlighter does not block React 19 via peerDependencies: as of v16.1.0 its peerDependencies is react: ">= 0.14.0", so React 19 satisfies the declared range and won’t fail installs due to peer-dep version checks. [1]

That said, “React 19 compatibility” is still tracked as an open request/issue in the repo (i.e., there isn’t an explicit “React 19 supported/tested” guarantee in the project’s issue tracker). [2]

Also note there are reported integration issues in React 19-era stacks (e.g., Next.js 15 + React 19-rc import/module problems), which may affect some setups even if the peer dependency range allows React 19. [3]

Practical take: If you’re upgrading to React 19, use the latest react-syntax-highlighter (currently 16.1.0) and expect it to generally work, but be prepared for tooling/bundler edge cases (especially with Next.js) and watch the open compatibility issues. [1][3]

Sources: [1] [2] [3]


Test react-syntax-highlighter@16.1.0 thoroughly with React 19 before merging.

The version 16.1.0 is the current latest (released Oct 28, 2025). However, React 19 compatibility is not officially tested or guaranteed by the library — while it meets the peer dependency range (react >= 0.14.0), there are reported integration issues in React 19 + Next.js stacks. Ensure the syntax highlighter works correctly in the new code block component under React 19.

🤖 Prompt for AI Agents
In `@apps/web/package.json` at line 64, The dependency "react-syntax-highlighter"
was bumped to 16.1.0 but its compatibility with React 19/Next.js is unverified;
run targeted tests of the new CodeBlock component and pages that render
highlighted code under React 19 (unit tests, Storybook, and an e2e run in the
Next.js dev/build server) and if you observe issues either pin the package to
the last known compatible version or add a compatibility shim/workaround; update
package.json and lockfile accordingly and include a short comment in
package.json or the PR describing the chosen remediation.

"tsx": "^4.20.3"
}
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Missing newline at end of file.

The file no longer ends with a newline character. Most editors and linters expect a trailing newline per POSIX convention.

🤖 Prompt for AI Agents
In `@apps/web/package.json` at line 90, The file apps/web/package.json is missing
a trailing newline (the file ends immediately after the final closing brace "}")
— add a single newline character at end-of-file so the file ends with a newline
to satisfy POSIX/editor/linter expectations.

Copy link
Contributor

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

🤖 Fix all issues with AI agents
In `@apps/web/features/editor/slash-menu/code-block/block.tsx`:
- Around line 86-97: handleBlur may read a stale localCode on quick blur after
typing; create a mutable ref (e.g., localCodeRef) and update it whenever you
call setLocalCode (or inside the onChange handler), then change handleBlur to
read localCodeRef.current instead of the closure-captured localCode when calling
editor.updateBlock for saving; keep containerRef and editor/block ids as before
and remove localCode from handleBlur's dependency list if you switch to the ref
so the callback doesn't re-create unnecessarily.

In `@apps/web/features/editor/slash-menu/file-tree/__tests__/utils.test.ts`:
- Around line 156-188: Add a round-trip test that builds a tree via
buildTreeFromFiles, calls serializeTreeToAscii then parseAsciiTree and asserts
the parsed structure matches the original (folder/file names and children
counts); then update parseAsciiTree to handle root-level entries that may not
start with box-drawing connectors (make leading connector characters optional
when computing depth and name) or alternatively make serializeTreeToAscii
include the same root-level connectors—ensure parseAsciiTree and
serializeTreeToAscii agree on whether depth-0 lines include "├──"/"└──" so the
round-trip test passes.

In `@apps/web/features/editor/slash-menu/file-tree/block.tsx`:
- Around line 257-267: The TreeProvider's initialState (expandedFolders via
collectAllFolderIds) is only used on mount so new folders stay collapsed; fix by
either forcing remount when nodes change (add a stable key on the TreeProvider
derived from nodes, e.g., a content hash or revision counter computed from nodes
and pass it as key) or by converting expandedFolders to a controlled prop (lift
state into this block: compute expandedFolders with collectAllFolderIds(nodes),
pass it as expandedFolders prop and add an onChange handler to update that
state) and update TreeProvider usage in both places (the block at the shown
snippet and the similar block at 284–294) so expanded state reflects config
saves.

In `@apps/web/features/editor/slash-menu/file-tree/config-modal.tsx`:
- Around line 467-506: The action buttons container in config-modal.tsx uses
"opacity-0 group-hover:opacity-100" which hides controls from keyboard users;
update the container's class (the div wrapping the buttons) to also include
"group-focus-within:opacity-100" (matching the pattern used in block.tsx) so the
add/delete buttons (FolderPlus, FilePlus, Trash2) become visible when the tree
node receives keyboard focus; ensure focusable elements inside (the button
elements) remain reachable/tabbable so keyboard users can discover and activate
them.

In `@apps/web/features/editor/slash-menu/file-tree/utils.ts`:
- Around line 85-156: The round-trip breaks because parseAsciiTree computes
depth by counting '│' characters; change depth calculation in parseAsciiTree to
derive depth from total indentation width instead: replace the current depth
assignment (using replace(/[^│]/g, '').length) with something like computing
indentLength = indent.replace(/\t/g, '    ').length and then depth =
Math.floor(indentLength / 4) so each 4-character indent level (e.g., '    ' or
'│   ') increments depth; update only the depth computation in parseAsciiTree to
use indent width.
🧹 Nitpick comments (5)
apps/web/features/editor/slash-menu/code-block/input-rule.tsx (1)

43-51: type: 'codeBlock' as any bypasses type safety.

Using as any here silences the compiler but hides potential mismatches between the block type string and the actual schema registration. If codeBlockSpec is properly registered in the editor schema, the correct approach is to type the editor generic or use the spec's type literal. This is acceptable as a pragmatic workaround for now given BlockNote's typing ergonomics, but worth a // TODO so it doesn't get lost.

apps/web/features/editor/slash-menu/file-tree/block.tsx (1)

69-95: BlockNote prop schema uses as TStyle cast — verify this is safe at runtime.

Line 79 casts the default to TStyle, but BlockNote stores prop values as plain strings. When reading back (Line 99: block.props.style as TStyle), there's no validation that the stored value is actually a member of TStyle. If the block schema evolves and old persisted data contains invalid values, this could silently pass an invalid style through.

Low risk currently since there's only one code path setting it, but a runtime guard (similar to the JSON parsing fallback at Line 104) would be more defensive.

apps/web/features/editor/slash-menu/file-tree/viewer.tsx (2)

42-52: setTimeout in handleCopy is not cleaned up on unmount.

If the component unmounts before the 2-second timeout fires, setCopied(false) will attempt to update state on an unmounted component. While React 19 silently ignores this, it's a minor resource leak.

Optional fix using a ref
+    const copyTimeoutRef = useRef<ReturnType<typeof setTimeout>>()
+
     const handleCopy = useCallback(async () => {
         if (!selectedNode?.content) return
         try {
             await navigator.clipboard.writeText(selectedNode.content)
             setCopied(true)
-            setTimeout(() => setCopied(false), 2000)
+            clearTimeout(copyTimeoutRef.current)
+            copyTimeoutRef.current = setTimeout(() => setCopied(false), 2000)
         } catch (err) {
             console.error('Failed to copy:', err)
         }
     }, [selectedNode])
+
+    useEffect(() => () => clearTimeout(copyTimeoutRef.current), [])

147-173: CodeViewer line-number table is functional but consider table-auto or removing table-fixed.

table-fixed (Line 153) distributes column width equally, which may cause the code column to clip content rather than allowing natural sizing. Since the parent has overflow-x-auto, switching to table-auto (or removing the fixed layout) would let long lines expand horizontally while still being scrollable.

apps/web/features/editor/slash-menu/file-tree/config-modal.tsx (1)

144-169: JSON import validation could be more thorough.

Line 156 checks data.name is a string and data.files is an array, but doesn't validate individual file entries (e.g., each file has a path string). Malformed files entries could cause buildTreeFromFiles to throw or produce broken nodes. Consider validating at least path existence:

if (!data.files.every((f: unknown) => typeof (f as TFile).path === 'string')) {
    console.error('Invalid file entries in JSON')
    return
}

@remcostoeten
Copy link
Owner Author

@coderabbitai full review @sourcery-ai review
I have addressed all feedback from the second round of reviews.
Summary of changes:

  • Fixed TS2339 in input-rule.tsx
  • Fixed stale closures and deletion guards in code-block
  • Added TreeProvider key for proper remounting
  • Improved config-modal accessibility and validation
  • Fixed utils.ts ASCII parsing depth logic (indent-based)
  • Added round-trip test in utils.test.ts (14/14 passing)
  • Verified react-syntax-highlighter compatibility
  • Fixed minor lint issues (globals.css, package.json)

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 12, 2026

@remcostoeten, thank you for addressing all the feedback! I appreciate the detailed summary of your fixes. I'll trigger a comprehensive review of the entire PR now.

Summary of changes you've addressed:

  • Fixed TS2339 in input-rule.tsx
  • Fixed stale closures and deletion guards in code-block
  • Added TreeProvider key for proper remounting
  • Improved config-modal accessibility and validation
  • Fixed utils.ts ASCII parsing depth logic (indent-based)
  • Added round-trip test in utils.test.ts (14/14 passing)
  • Verified react-syntax-highlighter compatibility
  • Fixed minor lint issues (globals.css, package.json)

Let me perform a complete review of all the changes.

✅ Actions performed

Full review triggered.

Copy link
Contributor

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

🤖 Fix all issues with AI agents
In `@apps/web/features/editor/slash-menu/code-block/block.tsx`:
- Around line 110-118: handleFileNameBlur currently reads localCode from the
closure which can be stale; change it to mirror handleBlur by using
localCodeRef.current instead of localCode when calling editor.updateBlock for
block.id (keep language, fileName logic using localFileName ||
getDefaultFilename(language)). Ensure the dependency array and closure no longer
rely on localCode so the updated code value is always the latest.

In `@apps/web/features/editor/slash-menu/file-tree/block.tsx`:
- Around line 140-152: handleSaveConfig currently rebuilds nodes with
setNodes(buildTreeFromFiles(updatedComponent.files)) but does not validate or
clear the existing selectedNode, leaving FileViewer showing a removed file;
update the handleSaveConfig callback to, after computing the new nodes, search
the new tree (using the same node id/type logic as buildTreeFromFiles) for the
currently selectedNode and call setSelectedNode(foundNode) or
setSelectedNode(null) if not found (or pick a sensible fallback like the parent
directory), ensuring you import/use the existing selectedNode and
setSelectedNode state updater and perform this validation immediately after
setNodes in handleSaveConfig.
- Around line 276-307: The right viewer panel (the ResizablePanel wrapping
FileViewer) is getting the defaultSize fallback which renders an inline flex: 0
0 50% and overrides the flex-1 class; update that ResizablePanel (the one
containing FileViewer) to use a grow mode instead of the defaultSize: either
pass an explicit prop (e.g. grow or flexGrow) or remove/default the defaultSize
so the component renders flex: 1 1 0% (or class-based flex-1) rather than a
fixed 50%; locate the ResizablePanel instance in the block component (the panel
after ResizableHandle in ResizablePanelGroup) and change its props so it
flex-grows to fill remaining space.

In `@apps/web/package.json`:
- Line 64: The package.json contains an inline JS-style comment after the
dependency entry ("react-syntax-highlighter": "^16.1.0") which is invalid JSON
and will break installs; remove the trailing comment text entirely (leave the
dependency entry as a plain JSON key/value) or relocate the compatibility note
to the PR description or a source file comment, ensuring the
"react-syntax-highlighter" dependency line remains valid JSON.
🧹 Nitpick comments (5)
apps/web/features/editor/slash-menu/code-block/block.tsx (1)

50-50: Unused ref: languageSelectorRef is declared but never referenced.

🧹 Remove unused ref
-			const languageSelectorRef = useRef<HTMLDivElement>(null)
apps/web/features/editor/slash-menu/file-tree/viewer.tsx (2)

24-47: Unused language variable inside the highlight effect.

Line 42 computes language but it's never used — the effect just passes selectedNode.content through. This is dead code inside the effect. The comment on line 44-45 explains the intent, so this is just a cleanliness nit.

♻️ Suggested cleanup
     useEffect(() => {
         if (!selectedNode || selectedNode.type === 'folder' || !selectedNode.content) {
             setHighlightedCode('')
             return
         }
 
-        const language = selectedNode.language || getLanguageFromPath(selectedNode.path)
-
-        // For now, use a simple code display
-        // Shiki integration can be added later for full syntax highlighting
+        // TODO: integrate Shiki for syntax highlighting using
+        // selectedNode.language || getLanguageFromPath(selectedNode.path)
         setHighlightedCode(selectedNode.content)
     }, [selectedNode])

220-246: minSize / maxSize props declared in the type but silently ignored by the component.

ResizablePanelProps declares minSize and maxSize, and the consumer in block.tsx (line 280) passes minSize={20}. However, the component body never reads or enforces these values — clamping only happens externally in handleResize. This is a misleading API surface.

Either remove these props from the type (since enforcement is external), or apply them inside the component.

Option A: Remove unused props from the type
 type ResizablePanelProps = {
     children: React.ReactNode
     defaultSize?: number
-    minSize?: number
-    maxSize?: number
     className?: string
     collapsible?: boolean
     collapsed?: boolean
 }
apps/web/features/editor/slash-menu/file-tree/block.tsx (2)

257-268: TreeProvider key only uses top-level node IDs — deeply nested changes may not trigger remount.

The key nodes.map(n => n.id).join(',') only considers top-level nodes. If a nested folder/file is added or removed without changing the top-level structure, the key remains the same and TreeProvider won't remount, leaving expandedFolders stale.

Consider using a deeper hash or a revision counter incremented on every save.

Example: use collectAllFolderIds for a deeper key
-                                        key={nodes.map(n => n.id).join(',')}
+                                        key={collectAllFolderIds(nodes).join(',')}

Or maintain a simple revision counter:

+const [treeRevision, setTreeRevision] = useState(0)
 // in handleSaveConfig:
+setTreeRevision(r => r + 1)
 // in TreeProvider:
-key={nodes.map(n => n.id).join(',')}
+key={treeRevision}

Also applies to: 284-297


172-178: Dead zone between 15–20 collapses the panel — verify this is intentional.

When newSize falls between 15 and 20 (e.g., user slowly drags left), the panel snaps to 0 (collapsed). Then Math.max(20, ...) prevents it from being between 0 and 20 on the way back up. This creates a snap-to-collapse UX. If intentional, a brief comment would help future readers.

Comment on lines +140 to +152
const handleSaveConfig = useCallback(
(updatedComponent: TComponent) => {
editor.updateBlock(block.id, {
props: {
content: JSON.stringify(updatedComponent),
style,
showIndentLines
}
})
setNodes(buildTreeFromFiles(updatedComponent.files))
},
[editor, block.id, style, showIndentLines]
)
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Stale selectedNode after config save when the selected file is removed.

handleSaveConfig rebuilds nodes from the updated component but never clears or re-validates selectedNode. If the user removes the currently selected file via the config modal, FileViewer will continue rendering the stale node's content.

Suggested fix
 const handleSaveConfig = useCallback(
     (updatedComponent: TComponent) => {
         editor.updateBlock(block.id, {
             props: {
                 content: JSON.stringify(updatedComponent),
                 style,
                 showIndentLines
             }
         })
-        setNodes(buildTreeFromFiles(updatedComponent.files))
+        const newNodes = buildTreeFromFiles(updatedComponent.files)
+        setNodes(newNodes)
+        // Clear selection if the previously selected file no longer exists
+        if (selectedNode && !findNodeByPath(newNodes, selectedNode.path)) {
+            setSelectedNode(null)
+        }
     },
-    [editor, block.id, style, showIndentLines]
+    [editor, block.id, style, showIndentLines, selectedNode]
 )
🤖 Prompt for AI Agents
In `@apps/web/features/editor/slash-menu/file-tree/block.tsx` around lines 140 -
152, handleSaveConfig currently rebuilds nodes with
setNodes(buildTreeFromFiles(updatedComponent.files)) but does not validate or
clear the existing selectedNode, leaving FileViewer showing a removed file;
update the handleSaveConfig callback to, after computing the new nodes, search
the new tree (using the same node id/type logic as buildTreeFromFiles) for the
currently selectedNode and call setSelectedNode(foundNode) or
setSelectedNode(null) if not found (or pick a sensible fallback like the parent
directory), ensuring you import/use the existing selectedNode and
setSelectedNode state updater and perform this validation immediately after
setNodes in handleSaveConfig.

Copy link
Contributor

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

🤖 Fix all issues with AI agents
In `@apps/web/features/editor/components/editor-wrapper.tsx`:
- Around line 263-272: The decorateInlineCodeElements function is making every
inline code element a keyboard tab stop and announcing it as a button; remove
the unconditional node.tabIndex = 0 and node.setAttribute('role', 'button') in
decorateInlineCodeElements, instead add a non-interactive marker (e.g.,
node.dataset.inlineCode = 'true' or node.setAttribute('role','group')) and only
set a focusable/tabIndex and an explicit role when the picker interaction is
activated; keep getCurrentVariant(node) for aria-labels when the element is
explicitly made interactive.
- Around line 229-248: The code calls the private API
editor._tiptapEditor.commands.setTextSelection in setCodeSelection
(fragile/unsupported); replace this with BlockNote's public APIs instead:
compute the appropriate block-level selection or cursor offsets from the DOM
targets (fromTarget/toTarget/toOffset) and call editor.setSelection(...) or
editor.setTextCursorPosition(...) with those mapped block IDs/offsets; if
BlockNote currently lacks a public inline-selection method, add a small
adapter/helper that maps DOM positions to BlockNote selection params and invoke
editor.setSelection, and remove direct access to editor._tiptapEditor
(optionally keep a guarded fallback that logs a warning only if the public API
is unavailable). Ensure you update setCodeSelection to reference only public
symbols (setCodeSelection, editor.setSelection, editor.setTextCursorPosition)
and remove editor._tiptapEditor usage.
- Around line 250-261: The cast "as any" bypasses type checking when calling
editor.addStyles in applyVariant; remove the cast and create a properly typed
styles object (e.g. const styles: Parameters<typeof editor.addStyles>[0] = {
code: true, inlineCodeVariant: variant }) and pass that to editor.addStyles,
keeping the rest of applyVariant (setCodeSelection, aria-label, and the
try/catch) unchanged; this uses the actual parameter type of editor.addStyles
and preserves type-safety for InlineCodeVariant.

In `@apps/web/features/editor/slash-menu/code-block/block.tsx`:
- Around line 43-49: The effect currently writes back the entire props snapshot
(props: { ...block.props, autoFocus: false }) which can overwrite concurrent
edits; change the update to only mutate autoFocus instead of spreading
block.props — e.g., call editor.updateBlock(block.id, { props: { autoFocus:
false } }) or fetch the latest block props inside the effect and merge only
autoFocus (e.g., const latest = editor.getBlock(block.id) then update with {
...latest.props, autoFocus: false }), ensuring updateBlock is invoked with only
the minimal change to avoid clobbering in-flight edits.

In `@apps/web/features/editor/slash-menu/code-block/renderer.tsx`:
- Around line 75-84: The empty catch in handleCopy silently swallows clipboard
errors; update the catch to accept the error (e.g., catch (err)) and log a
warning with the error details (for example via console.warn or the app logger)
so failures from navigator.clipboard.writeText(code) are visible; keep existing
setIsCopied/timeoutRef behavior but ensure the catch logs the error and
optionally provide a minimal user-facing fallback or message if available.
- Around line 196-205: The adjustHeight callback (textareaRef manipulation) is
forcing an inline height that conflicts with the textarea's h-full approach and
the mirror-based sizing; remove the adjustHeight function and its useEffect
(references: adjustHeight, textareaRef, useEffect) so the textarea can rely on
the mirror div and normal flow to determine container height, or alternatively
keep adjustHeight and remove the textarea's h-full class—choose one approach
consistently. Also ensure the HighlightLayer sizing (height: '100%') is
adjusted: either give its parent an explicit height when keeping inline heights,
or change HighlightLayer to use auto/inherit so it follows the mirror-driven
height. Verify layout sync after the change.

In `@apps/web/features/editor/slash-menu/file-tree/index.ts`:
- Around line 1-6: Remove the duplicate export of getLanguageFromPath from
utils.ts to avoid the naming conflict in the barrel index; open
apps/web/features/editor/slash-menu/file-tree/utils.ts, find the re-export line
that forwards getLanguageFromPath (the export coming from './types') and delete
that export so the symbol is only exported from types.ts, leaving index.ts's
export * from './types' and export * from './utils' intact.

In `@apps/web/features/editor/slash-menu/file-tree/viewer.tsx`:
- Around line 137-167: CodeViewer currently accepts a language prop but never
uses it, so consumers expect syntax highlighting that isn't present; fix by
wiring the language prop into a proper highlighter (or clearly remove/annotate
it). Update the CodeViewer function to either (a) integrate a client-side syntax
highlighter (e.g., react-syntax-highlighter or Prism) and pass language into the
highlighter when rendering each line while preserving the existing line-number
table layout, or (b) if highlighting is intentionally omitted, remove the
language prop from CodeViewer's signature and callers or add a concise comment
above CodeViewer explaining that language is unused and the component
intentionally renders plain text; reference CodeViewer and the language prop
when making the change.
- Around line 173-179: The MarkdownViewer component currently returns raw source
by wrapping the content prop in a <pre> (function MarkdownViewer) which prevents
parsing of headings/links/lists; either add an inline comment inside
MarkdownViewer clarifying the raw rendering is intentional, or replace the <pre>
rendering with a lightweight renderer (e.g., install/import react-markdown and
render <ReactMarkdown>{content}</ReactMarkdown>) and preserve the surrounding
prose classes for styling so markdown is parsed correctly; update tests/types if
any references assume rendered HTML.

In `@apps/web/features/editor/styles/editor.css`:
- Around line 231-290: Several hsl() uses in selectors like .editor-container
.sk-inline-code, .editor-container .sk-inline-code-picker and the variant blocks
use numeric alpha and bare hue values that violate stylelint; change numeric
alpha fractions to percentages (e.g. "/ 0.5" -> "/ 50%"; "/ 0.15" -> "/ 15%")
and add "deg" to literal hue numbers (e.g. "0" -> "0deg") while leaving var(...)
tokens intact (or ensure any CSS variables that represent hue include "deg" and
any variables for alpha are provided as percentages); update occurrences:
border: 1px solid hsl(var(--border) / 0.5), box-shadow/hsla usages like 0 6px
20px hsl(0 0% 0% / 0.15), and ring/destructive variants that use "/ 0.6", "/
0.18", "/ 0.12", "/ 0.4" so they use percentage alpha and deg where needed.
🧹 Nitpick comments (12)
apps/web/features/editor/styles/editor.css (1)

293-316: Stylelint flags sk-inline-code-picker__button as non-kebab-case.

The BEM double-underscore pattern is already used elsewhere in this file (e.g., .skriuw-mention-menu__item), so this is consistent with the existing convention. If the project's stylelint selector-class-pattern rule is intentionally strict about kebab-case, either update the rule to allow BEM or rename the element selector (e.g., .sk-inline-code-picker-button).

apps/web/features/editor/slash-menu/code-block/types.ts (1)

42-145: Language detection has overlapping patterns between Ruby and Python.

Both Ruby (\bdef\s+\w+) and Python (\bdef\s+\w+\s*\() use def as a keyword pattern. Python's version requires ( after the name, which is more specific, but a Ruby snippet with def foo(bar) would match both. Since scoring breaks ties by order and Python appears first, Python will win ties — which may be incorrect for Ruby code that uses parenthesized parameters. This is a minor heuristic limitation, not a blocker.

apps/web/features/editor/slash-menu/file-tree/components.tsx (2)

85-98: Context value object is re-created on every render.

The object literal passed to TreeContext.Provider at Line 87 creates a new reference on each render, which can trigger unnecessary re-renders for all context consumers even when the underlying values haven't changed. Consider wrapping in useMemo.

♻️ Proposed fix
+    const contextValue = useMemo(() => ({
+        state,
+        onToggleExpand,
+        onSelectFile,
+        showIndentLines,
+        enableHoverHighlight,
+        iconColorMode
+    }), [state, onToggleExpand, onSelectFile, showIndentLines, enableHoverHighlight, iconColorMode])
+
     return (
         <TreeContext.Provider
-            value={{
-                state,
-                onToggleExpand,
-                onSelectFile,
-                showIndentLines,
-                enableHoverHighlight,
-                iconColorMode
-            }}
+            value={contextValue}
         >

Note: Add useMemo to the React imports.


227-241: Redundant handleKeyDown on TreeNodeComponent — unreachable with tabIndex={-1}.

The handleKeyDown handler on individual tree items (Lines 227-241) duplicates logic already in Tree.handleKeyDown, but since individual items have tabIndex={-1} (Line 257), they never receive keyboard focus directly, making this handler dead code. Consider removing it for clarity.

apps/web/features/editor/slash-menu/file-tree/types.ts (1)

7-16: TNode.isExpanded is redundant with TTreeState.expandedFolders.

The isExpanded field on TNode (Line 15) is never read by the rendering components — they use state.expandedFolders.has(node.id) instead. It's only set during tree construction (buildTreeFromFiles and parseAsciiTree in utils.ts). If it serves as initial expansion state, consider documenting that intent; otherwise, it may confuse future maintainers.

apps/web/features/editor/slash-menu/file-tree/utils.ts (2)

70-90: Serializer omits connectors for root-level nodes, which is fine for display but fragile for round-tripping.

Root items have no connector prefix (depth === 0 ? '' at Line 79), meaning they have zero indentation. parseAsciiTree handles this correctly because zero indent → depth 0 → pushed to root. However, if a root folder's child is also at depth 0 (due to unusual input), the parser would incorrectly place it at the root level. Since the serializer always produces correct output, this is only a concern for hand-edited input.


176-186: updateNode unconditionally recurses into all children branches.

Even after the target node is found and updated, the function continues scanning other branches (Line 181-182). For large trees this is inefficient, but for typical file tree sizes it's negligible. A short-circuit could be added later if performance becomes a concern.

apps/web/features/editor/slash-menu/code-block/block.tsx (1)

4-4: useMemo is imported but never used.

useMemo is included in the React import but isn't used anywhere in this file.

♻️ Remove unused import
-import React, { useState, useCallback, useRef, useEffect, useMemo } from 'react'
+import React, { useState, useCallback, useRef, useEffect } from 'react'
apps/web/features/editor/hooks/useEditorConfig.ts (1)

137-146: Redundant settings in useMemo dependency array.

fontSize, fontFamily, lineHeight, and maxWidth are already destructured from settings and listed as individual deps. Including settings itself causes the memo to recompute on any settings change (e.g. unrelated preferences), not just the ones used.

Proposed fix
 	}, [
 		fontFamily,
 		fontSize,
 		hasMarkdownShortcuts,
 		hasSpellCheck,
 		hasWordWrap,
 		lineHeight,
-		maxWidth,
-		settings
+		maxWidth
 	])
apps/web/features/editor/components/editor-wrapper.tsx (1)

176-465: ~290-line imperative DOM effect is a maintainability concern.

This entire inline-code-variant picker is built with raw document.createElement calls, manual event wiring, and a MutationObserver — bypassing React's rendering model entirely. This makes the feature hard to test, reason about, and maintain. The CSS classes (sk-inline-code-picker, sk-inline-code-picker__button) set on raw DOM nodes won't be picked up by Tailwind's content scanner unless they're also referenced elsewhere or in a safelist.

Consider extracting this into a React component (e.g., a portal-based popover) in a future pass. For now, ensure the sk-inline-code-* classes are defined in a CSS file or Tailwind safelist.

As per coding guidelines, "Reuse @skriuw/ui components before creating one-off primitives."

apps/web/features/editor/slash-menu/code-block/renderer.tsx (1)

260-263: detectLanguage runs a regex battery on every keystroke.

resolvedLanguage depends on code, which updates on every keystroke. detectLanguage iterates through pattern arrays with multiple regex tests. Consider debouncing language detection separately or only running it when code is saved (the parent already debounces the save at 400ms).

apps/web/features/editor/slash-menu/file-tree/viewer.tsx (1)

201-227: minSize, maxSize, and collapsible props are declared but unused.

The ResizablePanelProps type declares minSize, maxSize, and collapsible (Lines 204-207) but none are used in the component body. This creates a misleading API — consumers might set these expecting them to have an effect.

Either implement clamping logic or remove the unused props from the type.

Proposed fix — remove unused props
 type ResizablePanelProps = {
     children: React.ReactNode
     defaultSize?: number
-    minSize?: number
-    maxSize?: number
     className?: string
-    collapsible?: boolean
     collapsed?: boolean
 }

Comment on lines +229 to +248
const setCodeSelection = (element: HTMLElement) => {
const view = editor.prosemirrorView
const textNodeWalker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT)
const firstTextNode = textNodeWalker.nextNode() as Text | null
let lastTextNode: Text | null = firstTextNode
let current = firstTextNode
while (current) {
lastTextNode = current
current = textNodeWalker.nextNode() as Text | null
}

const fromTarget = firstTextNode ?? element
const toTarget = lastTextNode ?? element
const toOffset = lastTextNode?.textContent?.length ?? toTarget.childNodes.length

const from = view.posAtDOM(fromTarget, 0)
const to = view.posAtDOM(toTarget, toOffset)
editor._tiptapEditor.commands.setTextSelection({ from, to })
editor.focus()
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

find . -name "package.json" -type f | head -5 | xargs -I {} grep -l "blocknote" {}

Repository: remcostoeten/skriuw

Length of output: 88


🌐 Web query:

BlockNoteEditor public API setTextSelection blocknote v0.44

💡 Result:

In BlockNote v0.44, BlockNoteEditor does not expose a public setTextSelection(...) API.

What BlockNote exposes publicly

  • Set cursor to a block: editor.setTextCursorPosition(blockId, "start" | "end") (blocknotejs.org)
  • Select whole blocks (block-range selection): editor.setSelection(startBlockId, endBlockId) (blocknotejs.org)

If you specifically need an inline/text selection

That’s a Tiptap command (editor.commands.setTextSelection(...)). (tiptap.dev)
BlockNote wraps Tiptap and (at least in the source) exposes the underlying instance as editor._tiptapEditor, so you can do:

// Inline/text selection via Tiptap (not BlockNote public API)
editor._tiptapEditor.commands.setTextSelection({ from: 5, to: 10 })
editor._tiptapEditor.commands.setTextSelection(10) // cursor at pos 10

_tiptapEditor exists on BlockNoteEditor but it’s underscored and effectively “internal/unstable”, so prefer BlockNote’s selection APIs when you can. (app.unpkg.com)

Citations:


🏁 Script executed:

rg "_tiptapEditor" --type typescript --type tsx -A 2 -B 2

Repository: remcostoeten/skriuw

Length of output: 97


🏁 Script executed:

cat apps/web/package.json | grep -A 2 -B 2 "blocknote"

Repository: remcostoeten/skriuw

Length of output: 212


🏁 Script executed:

rg "_tiptapEditor" --type tsx --type ts -A 2 -B 2

Repository: remcostoeten/skriuw

Length of output: 90


🏁 Script executed:

rg "_tiptapEditor" -A 2 -B 2

Repository: remcostoeten/skriuw

Length of output: 3059


Using internal _tiptapEditor API is fragile and unsupported.

Line 246 uses editor._tiptapEditor.commands.setTextSelection(...), accessing BlockNote's internal Tiptap instance. This is a private API (prefixed with _) and is documented as unstable. BlockNote v0.44 does not expose a public API for inline text selection—only setTextCursorPosition() and setSelection() for block-level operations. While this workaround is currently necessary, it remains fragile and may break on BlockNote version bumps.

🤖 Prompt for AI Agents
In `@apps/web/features/editor/components/editor-wrapper.tsx` around lines 229 -
248, The code calls the private API
editor._tiptapEditor.commands.setTextSelection in setCodeSelection
(fragile/unsupported); replace this with BlockNote's public APIs instead:
compute the appropriate block-level selection or cursor offsets from the DOM
targets (fromTarget/toTarget/toOffset) and call editor.setSelection(...) or
editor.setTextCursorPosition(...) with those mapped block IDs/offsets; if
BlockNote currently lacks a public inline-selection method, add a small
adapter/helper that maps DOM positions to BlockNote selection params and invoke
editor.setSelection, and remove direct access to editor._tiptapEditor
(optionally keep a guarded fallback that logs a warning only if the public API
is unavailable). Ensure you update setCodeSelection to reference only public
symbols (setCodeSelection, editor.setSelection, editor.setTextCursorPosition)
and remove editor._tiptapEditor usage.

Comment on lines +250 to +261
const applyVariant = (element: HTMLElement, variant: InlineCodeVariant) => {
try {
setCodeSelection(element)
editor.addStyles({
code: true,
inlineCodeVariant: variant
} as any)
element.setAttribute('aria-label', `Inline code (${variant} style)`)
} catch (error) {
console.warn('Failed to apply inline code variant:', error)
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

find . -type f -name "editor-wrapper.tsx" | head -20

Repository: remcostoeten/skriuw

Length of output: 119


🏁 Script executed:

cat -n ./apps/web/features/editor/components/editor-wrapper.tsx | sed -n '240,270p'

Repository: remcostoeten/skriuw

Length of output: 1441


🏁 Script executed:

cat -n ./apps/web/features/editor/components/editor-wrapper.tsx | head -50

Repository: remcostoeten/skriuw

Length of output: 2129


🏁 Script executed:

rg "addStyles" --type ts --type tsx -B 5 -A 10

Repository: remcostoeten/skriuw

Length of output: 90


🏁 Script executed:

rg "addStyles" -B 5 -A 10

Repository: remcostoeten/skriuw

Length of output: 1378


🏁 Script executed:

rg "BlockNoteEditor" --type ts -A 5 -B 5 | head -80

Repository: remcostoeten/skriuw

Length of output: 6050


🏁 Script executed:

cat ./package.json | rg "@blocknote" -A 1

Repository: remcostoeten/skriuw

Length of output: 45


🏁 Script executed:

rg "addStyles" -A 15 --context=10

Repository: remcostoeten/skriuw

Length of output: 1833


🏁 Script executed:

find . -type f -name "*.d.ts" | xargs grep -l "addStyles" 2>/dev/null

Repository: remcostoeten/skriuw

Length of output: 47


🏁 Script executed:

find . -type f \( -name "package.json" -o -name "tsconfig.json" \) | head -10

Repository: remcostoeten/skriuw

Length of output: 328


🏁 Script executed:

cat ./package.json | grep -A 20 "@blocknote"

Repository: remcostoeten/skriuw

Length of output: 45


🏁 Script executed:

cat ./apps/web/package.json | grep -A 20 "@blocknote"

Repository: remcostoeten/skriuw

Length of output: 800


🏁 Script executed:

rg "addStyles" --context=20 -A 20

Repository: remcostoeten/skriuw

Length of output: 1906


🏁 Script executed:

rg "InlineCodeVariant" -B 2 -A 5

Repository: remcostoeten/skriuw

Length of output: 7070


🏁 Script executed:

cat -n ./apps/web/features/editor/hooks/useEditorConfig.ts | head -100

Repository: remcostoeten/skriuw

Length of output: 3825


🏁 Script executed:

cat -n ./apps/web/features/editor/hooks/useEditorConfig.ts | sed -n '55,80p'

Repository: remcostoeten/skriuw

Length of output: 903


Remove as any cast and properly type the styles object.

The inlineCodeVariant style is correctly registered in the BlockNote schema (see useEditorConfig.ts lines 74–75), but the as any cast at line 256 unnecessarily bypasses type checking. Since inlineCodeVariant is a defined style spec with type 'inlineCodeVariant', the styles object should have a proper type annotation that matches BlockNote's expected signature rather than casting to any. This preserves type safety and helps catch issues at compile time instead of silently masking them.

🤖 Prompt for AI Agents
In `@apps/web/features/editor/components/editor-wrapper.tsx` around lines 250 -
261, The cast "as any" bypasses type checking when calling editor.addStyles in
applyVariant; remove the cast and create a properly typed styles object (e.g.
const styles: Parameters<typeof editor.addStyles>[0] = { code: true,
inlineCodeVariant: variant }) and pass that to editor.addStyles, keeping the
rest of applyVariant (setCodeSelection, aria-label, and the try/catch)
unchanged; this uses the actual parameter type of editor.addStyles and preserves
type-safety for InlineCodeVariant.

Comment on lines +263 to +272
const decorateInlineCodeElements = () => {
const inlineCodeElements = container.querySelectorAll('code:not(pre code)')
for (const node of inlineCodeElements) {
if (!(node instanceof HTMLElement)) continue
node.classList.add('sk-inline-code')
node.tabIndex = 0
node.setAttribute('role', 'button')
node.setAttribute('aria-label', `Inline code (${getCurrentVariant(node)} style)`)
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Setting role="button" and tabIndex={0} on inline <code> elements may conflict with contenteditable.

Every inline code span gets tabIndex={0} and role="button", which makes them tab-stops inside the contenteditable editor. This can disrupt normal text editing flow — Tab/Shift+Tab will stop on each code span instead of the editor handling it naturally. Additionally, role="button" is misleading since these elements display code, not trigger actions.

Consider using role="group" or a custom data- attribute instead, and only making them focusable via the picker interaction rather than always.

🤖 Prompt for AI Agents
In `@apps/web/features/editor/components/editor-wrapper.tsx` around lines 263 -
272, The decorateInlineCodeElements function is making every inline code element
a keyboard tab stop and announcing it as a button; remove the unconditional
node.tabIndex = 0 and node.setAttribute('role', 'button') in
decorateInlineCodeElements, instead add a non-interactive marker (e.g.,
node.dataset.inlineCode = 'true' or node.setAttribute('role','group')) and only
set a focusable/tabIndex and an explicit role when the picker interaction is
activated; keep getCurrentVariant(node) for aria-labels when the element is
explicitly made interactive.

Comment on lines +43 to +49
useEffect(() => {
if (autoFocus) {
editor.updateBlock(block.id, {
props: { ...block.props, autoFocus: false }
})
}
}, [autoFocus, editor, block.id, block.props])
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Auto-focus effect spreads all block props on update — may revert concurrent edits.

Line 46 does props: { ...block.props, autoFocus: false }, which writes back the current snapshot of all props. If a debounced save and this auto-focus clear race (e.g., user types immediately after block insertion), the autoFocus clear could overwrite the debounced code/language save with the original prop values.

Consider updating only the autoFocus prop:

♻️ Proposed fix
 			useEffect(() => {
 				if (autoFocus) {
 					editor.updateBlock(block.id, {
-						props: { ...block.props, autoFocus: false }
+						props: { autoFocus: false }
 					})
 				}
-			}, [autoFocus, editor, block.id, block.props])
+			}, [autoFocus, editor, block.id])
🤖 Prompt for AI Agents
In `@apps/web/features/editor/slash-menu/code-block/block.tsx` around lines 43 -
49, The effect currently writes back the entire props snapshot (props: {
...block.props, autoFocus: false }) which can overwrite concurrent edits; change
the update to only mutate autoFocus instead of spreading block.props — e.g.,
call editor.updateBlock(block.id, { props: { autoFocus: false } }) or fetch the
latest block props inside the effect and merge only autoFocus (e.g., const
latest = editor.getBlock(block.id) then update with { ...latest.props,
autoFocus: false }), ensuring updateBlock is invoked with only the minimal
change to avoid clobbering in-flight edits.

Comment on lines +75 to +84
const handleCopy = useCallback(async (e: React.MouseEvent) => {
e.stopPropagation()
e.preventDefault()
try {
await navigator.clipboard.writeText(code)
setIsCopied(true)
if (timeoutRef.current) clearTimeout(timeoutRef.current)
timeoutRef.current = setTimeout(() => setIsCopied(false), 2000)
} catch { }
}, [code])
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Empty catch block silently swallows clipboard errors.

Line 83 catches and ignores all errors from navigator.clipboard.writeText. At minimum, log a warning so users/developers know why copy failed (e.g., permissions denied, insecure context).

Proposed fix
-        } catch { }
+        } catch (err) {
+            console.warn('Failed to copy to clipboard:', err)
+        }
🤖 Prompt for AI Agents
In `@apps/web/features/editor/slash-menu/code-block/renderer.tsx` around lines 75
- 84, The empty catch in handleCopy silently swallows clipboard errors; update
the catch to accept the error (e.g., catch (err)) and log a warning with the
error details (for example via console.warn or the app logger) so failures from
navigator.clipboard.writeText(code) are visible; keep existing
setIsCopied/timeoutRef behavior but ensure the catch logs the error and
optionally provide a minimal user-facing fallback or message if available.

Comment on lines +196 to +205
const adjustHeight = useCallback(() => {
const textarea = textareaRef.current
if (!textarea) return
textarea.style.height = 'auto'
textarea.style.height = `${Math.max(60, textarea.scrollHeight)}px`
}, [])

useEffect(() => {
adjustHeight()
}, [code, adjustHeight])
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

cat -n apps/web/features/editor/slash-menu/code-block/renderer.tsx | head -300

Repository: remcostoeten/skriuw

Length of output: 12725


🏁 Script executed:

cat -n apps/web/features/editor/slash-menu/code-block/renderer.tsx | sed -n '300,320p'

Repository: remcostoeten/skriuw

Length of output: 964


🏁 Script executed:

cat -n apps/web/features/editor/slash-menu/code-block/renderer.tsx | sed -n '320,330p'

Repository: remcostoeten/skriuw

Length of output: 165


🏁 Script executed:

cat -n apps/web/features/editor/slash-menu/code-block/renderer.tsx | sed -n '113,165p'

Repository: remcostoeten/skriuw

Length of output: 2732


🏁 Script executed:

fd -e css -e scss apps/web/features/editor && head -50 apps/web/styles/globals.css

Repository: remcostoeten/skriuw

Length of output: 464


🏁 Script executed:

fd --full-path 'apps/web/features/editor' -e css

Repository: remcostoeten/skriuw

Length of output: 105


🏁 Script executed:

cat apps/web/features/editor/styles/editor.css && echo "---" && head -100 apps/web/styles/globals.css

Repository: remcostoeten/skriuw

Length of output: 24407


adjustHeight creates redundant inline height override conflicting with the h-full class.

The textarea uses absolute inset-0 w-full h-full, which should stretch to fill the relative parent. However, adjustHeight (lines 199–200) overrides this with an explicit textarea.style.height based on scrollHeight. Since the mirror div's content—using identical padding and styles—already determines the parent container's height in normal flow, this manual height adjustment appears redundant.

Verify that removing adjustHeight doesn't break layout synchronization, or remove the h-full class if explicit height control is intentional. Note that the HighlightLayer's height: '100%' (line 129) is ineffective because its parent has no explicit height constraint.

🤖 Prompt for AI Agents
In `@apps/web/features/editor/slash-menu/code-block/renderer.tsx` around lines 196
- 205, The adjustHeight callback (textareaRef manipulation) is forcing an inline
height that conflicts with the textarea's h-full approach and the mirror-based
sizing; remove the adjustHeight function and its useEffect (references:
adjustHeight, textareaRef, useEffect) so the textarea can rely on the mirror div
and normal flow to determine container height, or alternatively keep
adjustHeight and remove the textarea's h-full class—choose one approach
consistently. Also ensure the HighlightLayer sizing (height: '100%') is
adjusted: either give its parent an explicit height when keeping inline heights,
or change HighlightLayer to use auto/inherit so it follows the mirror-driven
height. Verify layout sync after the change.

Comment on lines +1 to +6
export { fileTreeBlockSpec } from './block'
export { ConfigModal } from './config-modal'
export { Tree, TreeProvider } from './components'
export { FileViewer, ResizablePanelGroup, ResizablePanel, ResizableHandle } from './viewer'
export * from './types'
export * from './utils'
Copy link
Contributor

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
# Verify both modules export getLanguageFromPath
echo "=== Exports from types.ts ==="
rg -n 'export.*getLanguageFromPath' apps/web/features/editor/slash-menu/file-tree/types.ts

echo ""
echo "=== Exports from utils.ts ==="
rg -n 'export.*getLanguageFromPath' apps/web/features/editor/slash-menu/file-tree/utils.ts

echo ""
echo "=== Full content of utils.ts to check context ==="
cat -n apps/web/features/editor/slash-menu/file-tree/utils.ts | head -20

echo ""
echo "=== Consumers that import getLanguageFromPath (anywhere in codebase) ==="
rg -n 'getLanguageFromPath' --type=ts --type=tsx | head -30

Repository: remcostoeten/skriuw

Length of output: 1191


🏁 Script executed:

#!/bin/bash
# Search for imports of getLanguageFromPath without restrictive file type
echo "=== All mentions of getLanguageFromPath in codebase ==="
rg -n 'getLanguageFromPath' --glob='!node_modules' | grep -v 'apps/web/features/editor/slash-menu/file-tree/'

echo ""
echo "=== Check barrel index.ts exports again ==="
cat -n apps/web/features/editor/slash-menu/file-tree/index.ts

echo ""
echo "=== Check types.ts around the export ==="
sed -n '95,105p' apps/web/features/editor/slash-menu/file-tree/types.ts

Repository: remcostoeten/skriuw

Length of output: 840


Duplicate getLanguageFromPath export from both ./types and ./utils wildcards.

utils.ts re-exports getLanguageFromPath from ./types (line 5 of utils.ts), and this barrel file does export * from './types' (line 5) and export * from './utils' (line 6). Both wildcards export the same symbol, which creates a naming conflict in the barrel and prevents consumers from importing it.

Fix by removing the re-export in utils.ts, since getLanguageFromPath is already exported directly from types.ts:

♻️ Remove redundant re-export from utils.ts

In apps/web/features/editor/slash-menu/file-tree/utils.ts:

-export { getLanguageFromPath }
🤖 Prompt for AI Agents
In `@apps/web/features/editor/slash-menu/file-tree/index.ts` around lines 1 - 6,
Remove the duplicate export of getLanguageFromPath from utils.ts to avoid the
naming conflict in the barrel index; open
apps/web/features/editor/slash-menu/file-tree/utils.ts, find the re-export line
that forwards getLanguageFromPath (the export coming from './types') and delete
that export so the symbol is only exported from types.ts, leaving index.ts's
export * from './types' and export * from './utils' intact.

Comment on lines +137 to +167
function CodeViewer({ code, language }: CodeViewerProps) {
const lines = code.split('\n')
const lineNumberWidth = String(lines.length).length

return (
<div className="font-mono text-sm w-full min-w-0 overflow-x-auto">
<pre>
<code>
<table className="w-full border-collapse table-fixed">
<tbody>
{lines.map((line, i) => (
<tr key={i} className="hover:bg-muted/30">
<td
className="px-4 py-0.5 text-right text-muted-foreground select-none border-r border-border/50 tabular-nums"
style={{ width: `${lineNumberWidth + 2}ch` }}
aria-hidden="true"
>
{i + 1}
</td>
<td className="px-4 py-0.5 whitespace-pre overflow-x-auto">
{line || '\u00A0'}
</td>
</tr>
))}
</tbody>
</table>
</code>
</pre>
</div>
)
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

CodeViewer accepts language but doesn't use it — no syntax highlighting.

The language prop is passed but the component just renders plain text with line numbers. If this is a placeholder for future highlighting, a brief comment would clarify intent. Otherwise consumers may expect colored output.

🤖 Prompt for AI Agents
In `@apps/web/features/editor/slash-menu/file-tree/viewer.tsx` around lines 137 -
167, CodeViewer currently accepts a language prop but never uses it, so
consumers expect syntax highlighting that isn't present; fix by wiring the
language prop into a proper highlighter (or clearly remove/annotate it). Update
the CodeViewer function to either (a) integrate a client-side syntax highlighter
(e.g., react-syntax-highlighter or Prism) and pass language into the highlighter
when rendering each line while preserving the existing line-number table layout,
or (b) if highlighting is intentionally omitted, remove the language prop from
CodeViewer's signature and callers or add a concise comment above CodeViewer
explaining that language is unused and the component intentionally renders plain
text; reference CodeViewer and the language prop when making the change.

Comment on lines +173 to +179
function MarkdownViewer({ content }: MarkdownViewerProps) {
return (
<div className="prose prose-sm dark:prose-invert max-w-none p-4">
<pre className="whitespace-pre-wrap font-sans">{content}</pre>
</div>
)
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

MarkdownViewer renders raw text, not parsed markdown.

Wrapping content in <pre> with font-sans and prose classes doesn't parse markdown — headings, links, lists, etc. will display as raw source. If this is intentional as a first pass, consider adding a comment. Otherwise, a lightweight markdown renderer (e.g., react-markdown) would be needed.

🤖 Prompt for AI Agents
In `@apps/web/features/editor/slash-menu/file-tree/viewer.tsx` around lines 173 -
179, The MarkdownViewer component currently returns raw source by wrapping the
content prop in a <pre> (function MarkdownViewer) which prevents parsing of
headings/links/lists; either add an inline comment inside MarkdownViewer
clarifying the raw rendering is intentional, or replace the <pre> rendering with
a lightweight renderer (e.g., install/import react-markdown and render
<ReactMarkdown>{content}</ReactMarkdown>) and preserve the surrounding prose
classes for styling so markdown is parsed correctly; update tests/types if any
references assume rendered HTML.

Comment on lines +231 to +290
color: hsl(var(--muted-foreground));
background: hsl(var(--muted));
padding: 0.15rem 0.4rem;
border-radius: 0.375rem;
border: 1px solid hsl(var(--border) / 0.5);
font-family: 'Fira Code', monospace;
font-size: 0.875em;
font-weight: 500;
transition:
background-color 120ms ease,
border-color 120ms ease,
color 120ms ease,
box-shadow 120ms ease;
}

.editor-container .sk-inline-code {
cursor: pointer;
outline: none;
}

.editor-container .sk-inline-code:hover,
.editor-container .sk-inline-code:focus-visible {
border-color: hsl(var(--ring) / 0.6);
box-shadow: 0 0 0 2px hsl(var(--ring) / 0.18);
}

.editor-container .sk-inline-code[data-inline-code-variant='default'],
.editor-container [data-style-type='inlineCodeVariant'][data-value='default'] code {
color: hsl(var(--muted-foreground));
background: hsl(var(--muted));
border-color: hsl(var(--border) / 0.5);
}

.editor-container .sk-inline-code[data-inline-code-variant='destructive'],
.editor-container [data-style-type='inlineCodeVariant'][data-value='destructive'] code {
color: hsl(var(--destructive));
background: hsl(var(--destructive) / 0.12);
border-color: hsl(var(--destructive) / 0.4);
}

.editor-container .sk-inline-code[data-inline-code-variant='outline'],
.editor-container [data-style-type='inlineCodeVariant'][data-value='outline'] code {
color: hsl(var(--foreground));
background: hsl(var(--background));
border-color: hsl(var(--border));
}

.editor-container .sk-inline-code-picker {
position: absolute;
z-index: 30;
display: inline-flex;
align-items: center;
gap: 0.2rem;
padding: 0.2rem;
border-radius: 0.5rem;
border: 1px solid hsl(var(--border));
background: hsl(var(--popover));
box-shadow:
0 6px 20px hsl(0 0% 0% / 0.15),
0 1px 0 hsl(0 0% 100% / 0.06) inset;
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Fix Stylelint alpha-value-notation and hue-degree-notation errors.

The linter expects percentage notation for alpha values and degree notation for hue values. There are multiple violations across the new rules.

♻️ Proposed fixes for the alpha/hue notation
-	border: 1px solid hsl(var(--border) / 0.5);
+	border: 1px solid hsl(var(--border) / 50%);
-	border-color: hsl(var(--ring) / 0.6);
-	box-shadow: 0 0 0 2px hsl(var(--ring) / 0.18);
+	border-color: hsl(var(--ring) / 60%);
+	box-shadow: 0 0 0 2px hsl(var(--ring) / 18%);
-	border-color: hsl(var(--border) / 0.5);
+	border-color: hsl(var(--border) / 50%);
-	background: hsl(var(--destructive) / 0.12);
-	border-color: hsl(var(--destructive) / 0.4);
+	background: hsl(var(--destructive) / 12%);
+	border-color: hsl(var(--destructive) / 40%);
-		0 6px 20px hsl(0 0% 0% / 0.15),
-		0 1px 0 hsl(0 0% 100% / 0.06) inset;
+		0 6px 20px hsl(0deg 0% 0% / 15%),
+		0 1px 0 hsl(0deg 0% 100% / 6%) inset;
📝 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
color: hsl(var(--muted-foreground));
background: hsl(var(--muted));
padding: 0.15rem 0.4rem;
border-radius: 0.375rem;
border: 1px solid hsl(var(--border) / 0.5);
font-family: 'Fira Code', monospace;
font-size: 0.875em;
font-weight: 500;
transition:
background-color 120ms ease,
border-color 120ms ease,
color 120ms ease,
box-shadow 120ms ease;
}
.editor-container .sk-inline-code {
cursor: pointer;
outline: none;
}
.editor-container .sk-inline-code:hover,
.editor-container .sk-inline-code:focus-visible {
border-color: hsl(var(--ring) / 0.6);
box-shadow: 0 0 0 2px hsl(var(--ring) / 0.18);
}
.editor-container .sk-inline-code[data-inline-code-variant='default'],
.editor-container [data-style-type='inlineCodeVariant'][data-value='default'] code {
color: hsl(var(--muted-foreground));
background: hsl(var(--muted));
border-color: hsl(var(--border) / 0.5);
}
.editor-container .sk-inline-code[data-inline-code-variant='destructive'],
.editor-container [data-style-type='inlineCodeVariant'][data-value='destructive'] code {
color: hsl(var(--destructive));
background: hsl(var(--destructive) / 0.12);
border-color: hsl(var(--destructive) / 0.4);
}
.editor-container .sk-inline-code[data-inline-code-variant='outline'],
.editor-container [data-style-type='inlineCodeVariant'][data-value='outline'] code {
color: hsl(var(--foreground));
background: hsl(var(--background));
border-color: hsl(var(--border));
}
.editor-container .sk-inline-code-picker {
position: absolute;
z-index: 30;
display: inline-flex;
align-items: center;
gap: 0.2rem;
padding: 0.2rem;
border-radius: 0.5rem;
border: 1px solid hsl(var(--border));
background: hsl(var(--popover));
box-shadow:
0 6px 20px hsl(0 0% 0% / 0.15),
0 1px 0 hsl(0 0% 100% / 0.06) inset;
color: hsl(var(--muted-foreground));
background: hsl(var(--muted));
padding: 0.15rem 0.4rem;
border-radius: 0.375rem;
border: 1px solid hsl(var(--border) / 50%);
font-family: 'Fira Code', monospace;
font-size: 0.875em;
font-weight: 500;
transition:
background-color 120ms ease,
border-color 120ms ease,
color 120ms ease,
box-shadow 120ms ease;
}
.editor-container .sk-inline-code {
cursor: pointer;
outline: none;
}
.editor-container .sk-inline-code:hover,
.editor-container .sk-inline-code:focus-visible {
border-color: hsl(var(--ring) / 60%);
box-shadow: 0 0 0 2px hsl(var(--ring) / 18%);
}
.editor-container .sk-inline-code[data-inline-code-variant='default'],
.editor-container [data-style-type='inlineCodeVariant'][data-value='default'] code {
color: hsl(var(--muted-foreground));
background: hsl(var(--muted));
border-color: hsl(var(--border) / 50%);
}
.editor-container .sk-inline-code[data-inline-code-variant='destructive'],
.editor-container [data-style-type='inlineCodeVariant'][data-value='destructive'] code {
color: hsl(var(--destructive));
background: hsl(var(--destructive) / 12%);
border-color: hsl(var(--destructive) / 40%);
}
.editor-container .sk-inline-code[data-inline-code-variant='outline'],
.editor-container [data-style-type='inlineCodeVariant'][data-value='outline'] code {
color: hsl(var(--foreground));
background: hsl(var(--background));
border-color: hsl(var(--border));
}
.editor-container .sk-inline-code-picker {
position: absolute;
z-index: 30;
display: inline-flex;
align-items: center;
gap: 0.2rem;
padding: 0.2rem;
border-radius: 0.5rem;
border: 1px solid hsl(var(--border));
background: hsl(var(--popover));
box-shadow:
0 6px 20px hsl(0deg 0% 0% / 15%),
0 1px 0 hsl(0deg 0% 100% / 6%) inset;
🧰 Tools
🪛 Stylelint (17.2.0)

[error] 235-235: Expected "0.5" to be "50%" (alpha-value-notation)

(alpha-value-notation)


[error] 253-253: Expected "0.6" to be "60%" (alpha-value-notation)

(alpha-value-notation)


[error] 254-254: Expected "0.18" to be "18%" (alpha-value-notation)

(alpha-value-notation)


[error] 261-261: Expected "0.5" to be "50%" (alpha-value-notation)

(alpha-value-notation)


[error] 267-267: Expected "0.12" to be "12%" (alpha-value-notation)

(alpha-value-notation)


[error] 268-268: Expected "0.4" to be "40%" (alpha-value-notation)

(alpha-value-notation)


[error] 289-289: Expected "0.15" to be "15%" (alpha-value-notation)

(alpha-value-notation)


[error] 290-290: Expected "0.06" to be "6%" (alpha-value-notation)

(alpha-value-notation)


[error] 289-289: Expected "0" to be "0deg" (hue-degree-notation)

(hue-degree-notation)


[error] 290-290: Expected "0" to be "0deg" (hue-degree-notation)

(hue-degree-notation)

🤖 Prompt for AI Agents
In `@apps/web/features/editor/styles/editor.css` around lines 231 - 290, Several
hsl() uses in selectors like .editor-container .sk-inline-code,
.editor-container .sk-inline-code-picker and the variant blocks use numeric
alpha and bare hue values that violate stylelint; change numeric alpha fractions
to percentages (e.g. "/ 0.5" -> "/ 50%"; "/ 0.15" -> "/ 15%") and add "deg" to
literal hue numbers (e.g. "0" -> "0deg") while leaving var(...) tokens intact
(or ensure any CSS variables that represent hue include "deg" and any variables
for alpha are provided as percentages); update occurrences: border: 1px solid
hsl(var(--border) / 0.5), box-shadow/hsla usages like 0 6px 20px hsl(0 0% 0% /
0.15), and ring/destructive variants that use "/ 0.6", "/ 0.18", "/ 0.12", "/
0.4" so they use percentage alpha and deg where needed.

@remcostoeten remcostoeten merged commit 29693a4 into daddy Feb 12, 2026
4 checks passed
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