Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 24 additions & 20 deletions src/components/tiptap-ui/blockquote-button/use-blockquote.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
"use client"

import { useCallback, useEffect, useState } from "react"
import { useCallback } from "react"
import type { Editor } from "@tiptap/react"
import { useEditorState } from "@tiptap/react"
import { NodeSelection, TextSelection } from "@tiptap/pm/state"

// --- Hooks ---
Expand Down Expand Up @@ -231,25 +232,28 @@ export function useBlockquote(config?: UseBlockquoteConfig) {
} = config || {}

const { editor } = useTiptapEditor(providedEditor)
const [isVisible, setIsVisible] = useState<boolean>(true)
const canToggle = canToggleBlockquote(editor)
const isActive = editor?.isActive("blockquote") || false

useEffect(() => {
if (!editor) return

const handleSelectionUpdate = () => {
setIsVisible(shouldShowButton({ editor, hideWhenUnavailable }))
}

handleSelectionUpdate()

editor.on("selectionUpdate", handleSelectionUpdate)

return () => {
editor.off("selectionUpdate", handleSelectionUpdate)
}
}, [editor, hideWhenUnavailable])
const toolbarState = useEditorState({
editor,
selector: ({ editor: currentEditor }) => {
if (!currentEditor) {
return {
isVisible: true,
isActive: false,
canToggle: false,
}
}

return {
isVisible: shouldShowButton({
editor: currentEditor,
hideWhenUnavailable,
}),
isActive: currentEditor.isActive("blockquote"),
canToggle: canToggleBlockquote(currentEditor),
}
},
})!
const { isVisible, isActive, canToggle } = toolbarState

const handleToggle = useCallback(() => {
if (!editor) return false
Expand Down
48 changes: 28 additions & 20 deletions src/components/tiptap-ui/code-block-button/use-code-block.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
"use client"

import { useCallback, useEffect, useState } from "react"
import { useCallback } from "react"
import { type Editor } from "@tiptap/react"
import { useEditorState } from "@tiptap/react"
import { NodeSelection, TextSelection } from "@tiptap/pm/state"

// --- Hooks ---
Expand Down Expand Up @@ -241,25 +242,32 @@ export function useCodeBlock(config?: UseCodeBlockConfig) {
} = config || {}

const { editor } = useTiptapEditor(providedEditor)
const [isVisible, setIsVisible] = useState<boolean>(true)
const canToggleState = canToggle(editor)
const isActive = editor?.isActive("codeBlock") || false

useEffect(() => {
if (!editor) return

const handleSelectionUpdate = () => {
setIsVisible(shouldShowButton({ editor, hideWhenUnavailable }))
}

handleSelectionUpdate()

editor.on("selectionUpdate", handleSelectionUpdate)

return () => {
editor.off("selectionUpdate", handleSelectionUpdate)
}
}, [editor, hideWhenUnavailable])
const toolbarState = useEditorState({
editor,
selector: ({ editor: currentEditor }) => {
if (!currentEditor) {
return {
isVisible: true,
isActive: false,
canToggle: false,
}
}

return {
isVisible: shouldShowButton({
editor: currentEditor,
hideWhenUnavailable,
}),
isActive: currentEditor.isActive("codeBlock"),
canToggle: canToggle(currentEditor),
}
},
})!
const {
isVisible,
isActive,
canToggle: canToggleState,
} = toolbarState

const handleToggle = useCallback(() => {
if (!editor) return false
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
"use client"

import { useCallback, useEffect, useState } from "react"
import { useCallback } from "react"
import { type Editor } from "@tiptap/react"
import { useEditorState } from "@tiptap/react"
import { useHotkeys } from "react-hotkeys-hook"

// --- Hooks ---
Expand Down Expand Up @@ -293,28 +294,36 @@ export function useColorHighlight(config: UseColorHighlightConfig) {

const { editor } = useTiptapEditor(providedEditor)
const isMobile = useIsBreakpoint()
const [isVisible, setIsVisible] = useState<boolean>(true)
const canColorHighlightState = canColorHighlight(editor, mode)
const actualColor = highlightColor
? getHighlightColorValue(highlightColor, useColorValue)
: highlightColor
const isActive = isColorHighlightActive(editor, actualColor, mode)

useEffect(() => {
if (!editor) return

const handleSelectionUpdate = () => {
setIsVisible(shouldShowButton({ editor, hideWhenUnavailable, mode }))
}

handleSelectionUpdate()

editor.on("selectionUpdate", handleSelectionUpdate)
const toolbarState = useEditorState({
editor,
selector: ({ editor: currentEditor }) => {
if (!currentEditor) {
return {
isVisible: true,
isActive: false,
canColorHighlight: false,
}
}

return () => {
editor.off("selectionUpdate", handleSelectionUpdate)
}
}, [editor, hideWhenUnavailable, mode])
return {
isVisible: shouldShowButton({
editor: currentEditor,
hideWhenUnavailable,
mode,
}),
isActive: isColorHighlightActive(currentEditor, actualColor, mode),
canColorHighlight: canColorHighlight(currentEditor, mode),
}
},
})!
const {
isVisible,
isActive,
canColorHighlight: canColorHighlightState,
} = toolbarState

const handleColorHighlight = useCallback(() => {
if (!editor || !canColorHighlightState || !actualColor || !label)
Expand Down
49 changes: 29 additions & 20 deletions src/components/tiptap-ui/heading-button/use-heading.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
"use client"

import { useCallback, useEffect, useState } from "react"
import { useCallback } from "react"
import { type Editor } from "@tiptap/react"
import { useEditorState } from "@tiptap/react"
import { NodeSelection, TextSelection } from "@tiptap/pm/state"

// --- Hooks ---
Expand Down Expand Up @@ -301,25 +302,33 @@ export function useHeading(config: UseHeadingConfig) {
} = config

const { editor } = useTiptapEditor(providedEditor)
const [isVisible, setIsVisible] = useState<boolean>(true)
const canToggleState = canToggle(editor, level)
const isActive = isHeadingActive(editor, level)

useEffect(() => {
if (!editor) return

const handleSelectionUpdate = () => {
setIsVisible(shouldShowButton({ editor, level, hideWhenUnavailable }))
}

handleSelectionUpdate()

editor.on("selectionUpdate", handleSelectionUpdate)

return () => {
editor.off("selectionUpdate", handleSelectionUpdate)
}
}, [editor, level, hideWhenUnavailable])
const toolbarState = useEditorState({
editor,
selector: ({ editor: currentEditor }) => {
if (!currentEditor) {
return {
isVisible: true,
isActive: false,
canToggle: false,
}
}

return {
isVisible: shouldShowButton({
editor: currentEditor,
level,
hideWhenUnavailable,
}),
isActive: isHeadingActive(currentEditor, level),
canToggle: canToggle(currentEditor, level),
}
},
})!
const {
isVisible,
isActive,
canToggle: canToggleState,
} = toolbarState

const handleToggle = useCallback(() => {
if (!editor) return false
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"use client"

import { useEffect, useState } from "react"
import type { Editor } from "@tiptap/react"
import { useEditorState } from "@tiptap/react"

// --- Hooks ---
import { useTiptapEditor } from "@/hooks/use-tiptap-editor"
Expand Down Expand Up @@ -96,29 +96,36 @@ export function useHeadingDropdownMenu(config?: UseHeadingDropdownMenuConfig) {
} = config || {}

const { editor } = useTiptapEditor(providedEditor)
const [isVisible, setIsVisible] = useState(true)
const toolbarState = useEditorState({
editor,
selector: ({ editor: currentEditor }) => {
if (!currentEditor) {
return {
isVisible: true,
activeLevel: undefined,
isActive: false,
canToggle: false,
}
}

const activeLevel = getActiveHeadingLevel(editor, levels)
const isActive = isHeadingActive(editor)
const canToggleState = canToggle(editor)

useEffect(() => {
if (!editor) return

const handleSelectionUpdate = () => {
setIsVisible(
shouldShowButton({ editor, hideWhenUnavailable, level: levels })
)
}

handleSelectionUpdate()

editor.on("selectionUpdate", handleSelectionUpdate)

return () => {
editor.off("selectionUpdate", handleSelectionUpdate)
}
}, [editor, hideWhenUnavailable, levels])
return {
isVisible: shouldShowButton({
editor: currentEditor,
hideWhenUnavailable,
level: levels,
}),
activeLevel: getActiveHeadingLevel(currentEditor, levels),
isActive: isHeadingActive(currentEditor, levels),
canToggle: canToggle(currentEditor),
}
},
})!
const {
isVisible,
activeLevel,
isActive,
canToggle: canToggleState,
} = toolbarState
Comment on lines +99 to +128

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Minor: canToggle(currentEditor) doesn't reflect the configured levels.

canToggle from heading-button accepts a single level and is only called here without it, so the result is a generic "can convert to any heading" check rather than scoped to the dropdown's configured levels (line 94 default [1..6], but consumers can pass a narrower subset). For a dropdown limited to e.g. [1, 2], canToggle may report true even if neither of those specific levels can actually be toggled.

If this distinction matters, consider:

canToggle: levels.some((l) => canToggle(currentEditor, l)),

Otherwise, this is consistent with prior behavior — flagging for awareness only.

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

In `@src/components/tiptap-ui/heading-dropdown-menu/use-heading-dropdown-menu.ts`
around lines 99 - 128, The selector passed to useEditorState uses
canToggle(currentEditor) without a level, which returns a generic "any-heading"
capability instead of reflecting the configured levels array; update the
selector so the canToggle property checks each configured level (levels.some(l
=> canToggle(currentEditor, l))) so the toolbarState.canToggle reflects whether
any of the dropdown's specific levels are toggleable; locate this logic inside
the selector callback in use-heading-dropdown-menu (symbols: useEditorState,
selector, levels, canToggle, toolbarState) and replace the single-call
canToggle(currentEditor) with a per-level check.


return {
isVisible,
Expand Down
43 changes: 24 additions & 19 deletions src/components/tiptap-ui/list-button/use-list.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
"use client"

import { useCallback, useEffect, useState } from "react"
import { useCallback } from "react"
import { type Editor } from "@tiptap/react"
import { useEditorState } from "@tiptap/react"
import { NodeSelection, TextSelection } from "@tiptap/pm/state"

// --- Hooks ---
Expand Down Expand Up @@ -309,25 +310,29 @@ export function useList(config: UseListConfig) {
} = config

const { editor } = useTiptapEditor(providedEditor)
const [isVisible, setIsVisible] = useState<boolean>(true)
const canToggle = canToggleList(editor, type)
const isActive = isListActive(editor, type)

useEffect(() => {
if (!editor) return

const handleSelectionUpdate = () => {
setIsVisible(shouldShowButton({ editor, type, hideWhenUnavailable }))
}

handleSelectionUpdate()

editor.on("selectionUpdate", handleSelectionUpdate)
const toolbarState = useEditorState({
editor,
selector: ({ editor: currentEditor }) => {
if (!currentEditor) {
return {
isVisible: true,
isActive: false,
canToggle: false,
}
}

return () => {
editor.off("selectionUpdate", handleSelectionUpdate)
}
}, [editor, type, hideWhenUnavailable])
return {
isVisible: shouldShowButton({
editor: currentEditor,
type,
hideWhenUnavailable,
}),
isActive: isListActive(currentEditor, type),
canToggle: canToggleList(currentEditor, type),
}
},
})!
const { isVisible, isActive, canToggle } = toolbarState
Comment on lines +313 to +335

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find the file and check its basic structure
find . -name "use-list.ts" -type f | head -5

Repository: ThinkEx-OSS/thinkex

Length of output: 114


🏁 Script executed:

#!/bin/bash
# Look for use-list-dropdown-menu.ts as well
find . -name "*use-list-dropdown-menu*" -type f

Repository: ThinkEx-OSS/thinkex

Length of output: 135


🏁 Script executed:

#!/bin/bash
# Check line count to understand file size
wc -l src/components/tiptap-ui/list-button/use-list.ts 2>/dev/null || echo "File not found at that path"

Repository: ThinkEx-OSS/thinkex

Length of output: 116


🏁 Script executed:

#!/bin/bash
# Read the selector code around lines 313-335
sed -n '310,340p' src/components/tiptap-ui/list-button/use-list.ts

Repository: ThinkEx-OSS/thinkex

Length of output: 826


🏁 Script executed:

#!/bin/bash
# Read shouldShowButton function (claimed to be around line 250)
sed -n '240,270p' src/components/tiptap-ui/list-button/use-list.ts

Repository: ThinkEx-OSS/thinkex

Length of output: 654


🏁 Script executed:

#!/bin/bash
# Get the full shouldShowButton function definition
rg -n "shouldShowButton" src/components/tiptap-ui/list-button/use-list.ts -A 20

Repository: ThinkEx-OSS/thinkex

Length of output: 1198


🏁 Script executed:

#!/bin/bash
# Check use-list-dropdown-menu.ts for similar selector pattern
cat -n src/components/tiptap-ui/list-dropdown-menu/use-list-dropdown-menu.ts | head -100

Repository: ThinkEx-OSS/thinkex

Length of output: 2968


🏁 Script executed:

#!/bin/bash
# Search for useEditorState usage in use-list-dropdown-menu.ts
rg -n "useEditorState" src/components/tiptap-ui/list-dropdown-menu/use-list-dropdown-menu.ts -A 25

Repository: ThinkEx-OSS/thinkex

Length of output: 1492


🏁 Script executed:

#!/bin/bash
# Look for any comments or context around the selector in use-list.ts that might explain the design
sed -n '305,325p' src/components/tiptap-ui/list-button/use-list.ts

Repository: ThinkEx-OSS/thinkex

Length of output: 524


🏁 Script executed:

#!/bin/bash
# Check if there's any git history or comments explaining the isVisible: true choice
git log -p --follow -S "isVisible: true" -- src/components/tiptap-ui/list-button/use-list.ts | head -80

Repository: ThinkEx-OSS/thinkex

Length of output: 45


🏁 Script executed:

#!/bin/bash
# Check for any related issue comments or design rationale in the file
sed -n '300,340p' src/components/tiptap-ui/list-button/use-list.ts

Repository: ThinkEx-OSS/thinkex

Length of output: 996


Align null-editor fallback in selector with shouldShowButton behavior to prevent render flicker.

The selector returns isVisible: true when the editor is null (lines 318-323), but shouldShowButton explicitly returns false for null editors (line 250). This inconsistency causes the button to appear visible briefly during initial editor setup or teardown, before shouldShowButton hides it—creating an unnecessary visual flicker.

The same pattern appears in use-list-dropdown-menu.ts (lines 178-185).

Change the null-editor fallback to isVisible: false to match shouldShowButton's behavior:

Suggested fix
   const toolbarState = useEditorState({
     editor,
     selector: ({ editor: currentEditor }) => {
       if (!currentEditor) {
         return {
-          isVisible: true,
+          isVisible: false,
           isActive: false,
           canToggle: false,
         }
       }

Apply the same fix to use-list-dropdown-menu.ts.

📝 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
const toolbarState = useEditorState({
editor,
selector: ({ editor: currentEditor }) => {
if (!currentEditor) {
return {
isVisible: true,
isActive: false,
canToggle: false,
}
}
return () => {
editor.off("selectionUpdate", handleSelectionUpdate)
}
}, [editor, type, hideWhenUnavailable])
return {
isVisible: shouldShowButton({
editor: currentEditor,
type,
hideWhenUnavailable,
}),
isActive: isListActive(currentEditor, type),
canToggle: canToggleList(currentEditor, type),
}
},
})
const { isVisible, isActive, canToggle } = toolbarState
const toolbarState = useEditorState({
editor,
selector: ({ editor: currentEditor }) => {
if (!currentEditor) {
return {
isVisible: false,
isActive: false,
canToggle: false,
}
}
return {
isVisible: shouldShowButton({
editor: currentEditor,
type,
hideWhenUnavailable,
}),
isActive: isListActive(currentEditor, type),
canToggle: canToggleList(currentEditor, type),
}
},
})
const { isVisible, isActive, canToggle } = toolbarState
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/tiptap-ui/list-button/use-list.ts` around lines 313 - 335, The
selector passed to useEditorState currently returns isVisible: true when the
editor is null, which conflicts with shouldShowButton (which returns false for
null editors) and causes a flicker; update the null-editor fallback inside the
selector (the block that returns { isVisible: true, isActive: false, canToggle:
false }) to return isVisible: false instead so isVisible, isActive and canToggle
align with shouldShowButton/isListActive/canToggleList behavior; apply the same
change in the analogous selector in use-list-dropdown-menu.ts to keep both
toolbarState and the dropdown menu consistent.


const handleToggle = useCallback(() => {
if (!editor) return false
Expand Down
Loading
Loading