diff --git a/apps/desktop/layer/renderer/src/components/ui/background/WindowUnderBlur.tsx b/apps/desktop/layer/renderer/src/components/ui/background/WindowUnderBlur.tsx
index 56899f9f4..6c4d03a08 100644
--- a/apps/desktop/layer/renderer/src/components/ui/background/WindowUnderBlur.tsx
+++ b/apps/desktop/layer/renderer/src/components/ui/background/WindowUnderBlur.tsx
@@ -1,4 +1,4 @@
-import { Focusable } from "@follow/components/common/Focusable.js"
+import { Focusable } from "@follow/components/common/Focusable/index.js"
import { SYSTEM_CAN_UNDER_BLUR_WINDOW } from "@follow/shared/constants"
import { cn } from "@follow/utils/utils"
diff --git a/apps/desktop/layer/renderer/src/components/ui/media/VideoPlayer.tsx b/apps/desktop/layer/renderer/src/components/ui/media/VideoPlayer.tsx
index 14532ae82..afa8d7b64 100644
--- a/apps/desktop/layer/renderer/src/components/ui/media/VideoPlayer.tsx
+++ b/apps/desktop/layer/renderer/src/components/ui/media/VideoPlayer.tsx
@@ -1,4 +1,4 @@
-import { Focusable } from "@follow/components/common/Focusable.js"
+import { Focusable } from "@follow/components/common/Focusable/index.js"
import { Spring } from "@follow/components/constants/spring.js"
import { ActionButton, MotionButtonBase } from "@follow/components/ui/button/index.js"
import type { HTMLMediaState } from "@follow/hooks"
diff --git a/apps/desktop/layer/renderer/src/modules/command/commands/id.ts b/apps/desktop/layer/renderer/src/modules/command/commands/id.ts
index dc5264fb7..da71ab50d 100644
--- a/apps/desktop/layer/renderer/src/modules/command/commands/id.ts
+++ b/apps/desktop/layer/renderer/src/modules/command/commands/id.ts
@@ -46,11 +46,13 @@ export const COMMAND_ID = {
},
layout: {
toggleTimelineColumn: "layout:toggle-timeline-column",
+ focusToTimeline: "layout:focus-to-timeline",
},
timeline: {
switchToNext: "timeline:switch-to-next",
switchToPrevious: "timeline:switch-to-previous",
refetch: "timeline:refetch",
+ enter: "timeline:enter",
},
entryRender: {
scrollDown: "entry-render:scroll-down",
diff --git a/apps/desktop/layer/renderer/src/modules/command/commands/layout.tsx b/apps/desktop/layer/renderer/src/modules/command/commands/layout.tsx
index 309d3811f..00e3e6a64 100644
--- a/apps/desktop/layer/renderer/src/modules/command/commands/layout.tsx
+++ b/apps/desktop/layer/renderer/src/modules/command/commands/layout.tsx
@@ -1,9 +1,17 @@
+import { EventBus } from "@follow/utils/event-bus"
+
import { setTimelineColumnShow } from "~/atoms/sidebar"
import { useRegisterCommandEffect } from "../hooks/use-register-command"
import type { Command } from "../types"
import { COMMAND_ID } from "./id"
+declare module "@follow/utils/event-bus" {
+ interface EventBusMap {
+ "layout:focus-to-timeline": never
+ }
+}
+
export const useRegisterLayoutCommands = () => {
useRegisterCommandEffect([
{
@@ -13,6 +21,13 @@ export const useRegisterLayoutCommands = () => {
setTimelineColumnShow((show) => !show)
},
},
+ {
+ id: COMMAND_ID.layout.focusToTimeline,
+ label: "Focus to timeline",
+ run: () => {
+ EventBus.dispatch(COMMAND_ID.layout.focusToTimeline)
+ },
+ },
])
}
@@ -21,4 +36,9 @@ export type ToggleTimelineColumnCommand = Command<{
fn: () => void
}>
-export type LayoutCommand = ToggleTimelineColumnCommand
+export type FocusToTimelineCommand = Command<{
+ id: typeof COMMAND_ID.layout.focusToTimeline
+ fn: () => void
+}>
+
+export type LayoutCommand = ToggleTimelineColumnCommand | FocusToTimelineCommand
diff --git a/apps/desktop/layer/renderer/src/modules/command/commands/timeline.tsx b/apps/desktop/layer/renderer/src/modules/command/commands/timeline.tsx
index f9e883cd3..3f6d9e809 100644
--- a/apps/desktop/layer/renderer/src/modules/command/commands/timeline.tsx
+++ b/apps/desktop/layer/renderer/src/modules/command/commands/timeline.tsx
@@ -9,6 +9,7 @@ declare module "@follow/utils/event-bus" {
"timeline:switch-to-next": never
"timeline:switch-to-previous": never
"timeline:refetch": never
+ "timeline:enter": never
}
}
export const useRegisterTimelineCommand = () => {
@@ -35,6 +36,13 @@ export const useRegisterTimelineCommand = () => {
EventBus.dispatch("timeline:refetch")
},
},
+ {
+ id: COMMAND_ID.timeline.enter,
+ label: "Enter Selected Entry",
+ run: () => {
+ EventBus.dispatch("timeline:enter")
+ },
+ },
])
}
@@ -53,7 +61,13 @@ export type RefetchTimelineCommand = Command<{
fn: () => void
}>
+export type EnterTimelineCommand = Command<{
+ id: typeof COMMAND_ID.timeline.enter
+ fn: () => void
+}>
+
export type TimelineCommand =
| SwitchToNextTimelineCommand
| SwitchToPreviousTimelineCommand
| RefetchTimelineCommand
+ | EnterTimelineCommand
diff --git a/apps/desktop/layer/renderer/src/modules/entry-column/EntryColumnShortcutHandler.tsx b/apps/desktop/layer/renderer/src/modules/entry-column/EntryColumnShortcutHandler.tsx
index 2d0119c89..f8f710790 100644
--- a/apps/desktop/layer/renderer/src/modules/entry-column/EntryColumnShortcutHandler.tsx
+++ b/apps/desktop/layer/renderer/src/modules/entry-column/EntryColumnShortcutHandler.tsx
@@ -1,9 +1,11 @@
+import { useFocusable, useFocusActions } from "@follow/components/common/Focusable/hooks.js"
+import { useScrollViewElement } from "@follow/components/ui/scroll-area/hooks.js"
import { useRefValue } from "@follow/hooks"
+import { nextFrame } from "@follow/utils/dom"
import { EventBus } from "@follow/utils/event-bus"
import type { FC } from "react"
-import { memo, useEffect, useLayoutEffect, useState } from "react"
+import { memo, useEffect } from "react"
-import { useMainContainerElement } from "~/atoms/dom"
import { HotkeyScope } from "~/constants"
import { useNavigateEntry } from "~/hooks/biz/useNavigateEntry"
import { useRouteEntryId } from "~/hooks/biz/useRouteParams"
@@ -11,7 +13,7 @@ import { useConditionalHotkeyScope } from "~/hooks/common"
import { useHotkeyScope } from "~/providers/hotkey-provider"
import { COMMAND_ID } from "../command/commands/id"
-import { useCommandBinding } from "../command/hooks/use-register-hotkey"
+import { useCommandBinding, useCommandHotkey } from "../command/hooks/use-register-hotkey"
export const EntryColumnShortcutHandler: FC<{
refetch: () => void
@@ -40,6 +42,12 @@ export const EntryColumnShortcutHandler: FC<{
when,
})
+ useCommandHotkey({
+ commandId: COMMAND_ID.timeline.enter,
+ shortcut: "Enter",
+ when,
+ })
+
const currentEntryIdRef = useRefValue(useRouteEntryId())
const navigate = useNavigateEntry()
@@ -82,32 +90,18 @@ export const EntryColumnShortcutHandler: FC<{
})
}, [refetch])
- const $mainContainer = useMainContainerElement()
- const [isFocusIn, setIsFocusIn] = useState(false)
+ const $scrollArea = useScrollViewElement()
+ const { highlightBoundary } = useFocusActions()
+ useEffect(() => {
+ return EventBus.subscribe(COMMAND_ID.layout.focusToTimeline, () => {
+ $scrollArea?.focus()
+ nextFrame(highlightBoundary)
+ })
+ }, [$scrollArea, highlightBoundary])
+
+ const isFocusIn = useFocusable()
useConditionalHotkeyScope(HotkeyScope.Timeline, isFocusIn, true)
- // Enable arrow key navigation shortcuts only when focus is on entryContent or entryList,
- // entryList shortcuts should not be triggered in the feed col
- useLayoutEffect(() => {
- if (!$mainContainer) return
- const handler = () => {
- const target = document.activeElement
-
- const isFocusIn = $mainContainer.contains(target) || $mainContainer === target
-
- setIsFocusIn(isFocusIn)
- }
-
- handler()
- // NOTE: focusin event will bubble to the document
- document.addEventListener("focusin", handler)
- document.addEventListener("focusout", handler)
- return () => {
- document.removeEventListener("focusin", handler)
- document.removeEventListener("focusout", handler)
- }
- }, [$mainContainer])
-
return null
})
diff --git a/apps/desktop/layer/renderer/src/modules/entry-column/index.tsx b/apps/desktop/layer/renderer/src/modules/entry-column/index.tsx
index c3d03232e..744a0acfb 100644
--- a/apps/desktop/layer/renderer/src/modules/entry-column/index.tsx
+++ b/apps/desktop/layer/renderer/src/modules/entry-column/index.tsx
@@ -1,3 +1,4 @@
+import { Focusable } from "@follow/components/common/Focusable/index.js"
import { useMobile } from "@follow/components/hooks/useMobile.js"
import { FeedViewType, views } from "@follow/constants"
import { useTitle } from "@follow/hooks"
@@ -116,7 +117,7 @@ function EntryColumnImpl() {
const ListComponent = views[view]!.gridMode ? EntryColumnGrid : EntryList
return (
-
)}
-
+
)
}
diff --git a/apps/desktop/layer/renderer/src/modules/entry-content/index.electron.tsx b/apps/desktop/layer/renderer/src/modules/entry-content/index.electron.tsx
index 2e6a0f1db..392258aa5 100644
--- a/apps/desktop/layer/renderer/src/modules/entry-content/index.electron.tsx
+++ b/apps/desktop/layer/renderer/src/modules/entry-content/index.electron.tsx
@@ -1,10 +1,14 @@
-import { Focusable, useFocusable } from "@follow/components/common/Focusable.js"
+import {
+ Focusable,
+ useFocusable,
+ useFocusActions,
+} from "@follow/components/common/Focusable/index.js"
import { MemoedDangerousHTMLStyle } from "@follow/components/common/MemoedDangerousHTMLStyle.js"
import { ScrollArea } from "@follow/components/ui/scroll-area/index.js"
import type { FeedViewType } from "@follow/constants"
import { useTitle } from "@follow/hooks"
import type { FeedModel, InboxModel } from "@follow/models/types"
-import { stopPropagation } from "@follow/utils/dom"
+import { nextFrame, stopPropagation } from "@follow/utils/dom"
import { EventBus } from "@follow/utils/event-bus"
import { springScrollTo } from "@follow/utils/scroller"
import { cn, combineCleanupFunctions } from "@follow/utils/utils"
@@ -35,7 +39,7 @@ import { useFeedById } from "~/store/feed"
import { useInboxById } from "~/store/inbox"
import { COMMAND_ID } from "../command/commands/id"
-import { useCommandBinding } from "../command/hooks/use-register-hotkey"
+import { useCommandBinding, useCommandHotkey } from "../command/hooks/use-register-hotkey"
import { EntryContentHTMLRenderer } from "../renderer/html"
import { AISummary } from "./AISummary"
import { EntryTimelineSidebar } from "./components/EntryTimelineSidebar"
@@ -136,9 +140,9 @@ export const EntryContent: Component = ({
onFocus={() => setIsUserInteraction(true)}
>
@@ -299,10 +303,11 @@ const Renderer: React.FC<{
const RegisterCommands = ({
scrollerRef,
isUserInteraction,
+ setIsUserInteraction,
}: {
- entryId: string
scrollerRef: React.RefObject
isUserInteraction: boolean
+ setIsUserInteraction: (isUserInteraction: boolean) => void
}) => {
const containerFocused = useFocusable()
useConditionalHotkeyScope(HotkeyScope.EntryRender, isUserInteraction && containerFocused, true)
@@ -330,26 +335,43 @@ const RegisterCommands = ({
when,
})
- useEffect(() => {
- return EventBus.subscribe(COMMAND_ID.entryRender.scrollUp, () => {
- const currentScroll = scrollerRef.current?.scrollTop
- const delta = window.innerHeight
+ useCommandHotkey({
+ commandId: COMMAND_ID.layout.focusToTimeline,
+ when,
+ shortcut: "Backspace",
+ })
- if (typeof currentScroll === "number" && delta) {
- springScrollTo(currentScroll - delta, scrollerRef.current!)
- }
- })
- }, [scrollerRef])
+ const { highlightBoundary } = useFocusActions()
useEffect(() => {
- return EventBus.subscribe(COMMAND_ID.entryRender.scrollDown, () => {
- const currentScroll = scrollerRef.current?.scrollTop
- const delta = window.innerHeight
- if (typeof currentScroll === "number" && delta) {
- springScrollTo(currentScroll + delta, scrollerRef.current!)
- }
- })
- }, [scrollerRef])
+ return combineCleanupFunctions(
+ EventBus.subscribe(COMMAND_ID.entryRender.scrollUp, () => {
+ const currentScroll = scrollerRef.current?.scrollTop
+ const delta = window.innerHeight
+
+ if (typeof currentScroll === "number" && delta) {
+ springScrollTo(currentScroll - delta, scrollerRef.current!)
+ }
+ }),
+
+ EventBus.subscribe(COMMAND_ID.entryRender.scrollDown, () => {
+ const currentScroll = scrollerRef.current?.scrollTop
+ const delta = window.innerHeight
+ if (typeof currentScroll === "number" && delta) {
+ springScrollTo(currentScroll + delta, scrollerRef.current!)
+ }
+ }),
+ EventBus.subscribe(COMMAND_ID.timeline.enter, () => {
+ const $scroller = scrollerRef.current
+ if ($scroller) {
+ springScrollTo(0, $scroller)
+ $scroller.focus()
+ nextFrame(highlightBoundary)
+ setIsUserInteraction(true)
+ }
+ }),
+ )
+ }, [highlightBoundary, scrollerRef, setIsUserInteraction])
return null
}
diff --git a/apps/desktop/layer/renderer/src/modules/player/corner-player.tsx b/apps/desktop/layer/renderer/src/modules/player/corner-player.tsx
index fb578f721..5d0c9d3b9 100644
--- a/apps/desktop/layer/renderer/src/modules/player/corner-player.tsx
+++ b/apps/desktop/layer/renderer/src/modules/player/corner-player.tsx
@@ -1,4 +1,4 @@
-import { useFocusable } from "@follow/components/common/Focusable.js"
+import { useFocusable } from "@follow/components/common/Focusable/index.js"
import { Spring } from "@follow/components/constants/spring.js"
import { useMobile } from "@follow/components/hooks/useMobile.js"
import { Tooltip, TooltipContent, TooltipTrigger } from "@follow/components/ui/tooltip/index.jsx"
diff --git a/packages/internal/components/src/common/Focusable.tsx b/packages/internal/components/src/common/Focusable/Focusable.tsx
similarity index 50%
rename from packages/internal/components/src/common/Focusable.tsx
rename to packages/internal/components/src/common/Focusable/Focusable.tsx
index 50fbca1b6..e04ba6bd5 100644
--- a/packages/internal/components/src/common/Focusable.tsx
+++ b/packages/internal/components/src/common/Focusable/Focusable.tsx
@@ -1,9 +1,9 @@
-import { createContext, use, useImperativeHandle, useRef, useState } from "react"
+import { useCallback, useImperativeHandle, useMemo, useRef, useState } from "react"
import { useEventListener } from "usehooks-ts"
-// const
-const FocusableContext = createContext(false)
-const FocusTargetRefContext = createContext>(null!)
+import { FocusableContext, FocusActionsContext, FocusTargetRefContext } from "./context"
+import { highlightElement } from "./utils"
+
export const Focusable: Component<
React.DetailedHTMLProps, HTMLDivElement>
> = ({ ref, ...props }) => {
@@ -13,33 +13,35 @@ export const Focusable: Component<
const containerRef = useRef(null)
useImperativeHandle(ref, () => containerRef.current!)
+
+ const highlightBoundary = useCallback(() => {
+ if (!isFocusWithIn) return
+ const element = containerRef.current
+ if (!element) return
+
+ highlightElement(element)
+ }, [isFocusWithIn])
+
useEventListener("focusin", (e) => {
if (containerRef.current?.contains(e.target as Node)) {
setIsFocusWithIn(true)
+ focusTargetRef.current = e.target as HTMLElement
+ if (import.meta.env.DEV) {
+ highlightElement(containerRef.current!)
+ }
} else {
setIsFocusWithIn(false)
+ focusTargetRef.current = undefined
}
})
- // useEventListener("focusout", (e) => {
- // if (!containerRef.current?.contains(e.target as Node)) {
- // setIsFocusWithIn(false)
- // }
- // })
-
return (
-
+ ({ highlightBoundary }), [highlightBoundary])}>
+
+
)
}
-
-export const useFocusable = () => {
- return use(FocusableContext)
-}
-
-export const useFocusTargetRef = () => {
- return use(FocusTargetRefContext)
-}
diff --git a/packages/internal/components/src/common/Focusable/context.ts b/packages/internal/components/src/common/Focusable/context.ts
new file mode 100644
index 000000000..c50aa988d
--- /dev/null
+++ b/packages/internal/components/src/common/Focusable/context.ts
@@ -0,0 +1,7 @@
+import { createContext } from "react"
+
+export const FocusableContext = createContext(false)
+export const FocusTargetRefContext = createContext>(null!)
+export const FocusActionsContext = createContext<{
+ highlightBoundary: () => void
+}>(null!)
diff --git a/packages/internal/components/src/common/Focusable/hooks.ts b/packages/internal/components/src/common/Focusable/hooks.ts
new file mode 100644
index 000000000..4a349a1e5
--- /dev/null
+++ b/packages/internal/components/src/common/Focusable/hooks.ts
@@ -0,0 +1,15 @@
+import { use } from "react"
+
+import { FocusableContext, FocusActionsContext, FocusTargetRefContext } from "./context"
+
+export const useFocusable = () => {
+ return use(FocusableContext)
+}
+
+export const useFocusTargetRef = () => {
+ return use(FocusTargetRefContext)
+}
+
+export const useFocusActions = () => {
+ return use(FocusActionsContext)
+}
diff --git a/packages/internal/components/src/common/Focusable/index.ts b/packages/internal/components/src/common/Focusable/index.ts
new file mode 100644
index 000000000..b96ce5b87
--- /dev/null
+++ b/packages/internal/components/src/common/Focusable/index.ts
@@ -0,0 +1,2 @@
+export * from "./Focusable"
+export * from "./hooks"
diff --git a/packages/internal/components/src/common/Focusable/utils.ts b/packages/internal/components/src/common/Focusable/utils.ts
new file mode 100644
index 000000000..fd95891f4
--- /dev/null
+++ b/packages/internal/components/src/common/Focusable/utils.ts
@@ -0,0 +1,174 @@
+// Grok AI
+// Interface for keyframe properties
+interface HighlightKeyframe {
+ shadowWidth: number
+ shadowOpacity: number
+ outlineWidth: number
+ outlineOpacity: number
+}
+
+// Cubic-bezier easing function (0.4, 0, 0.2, 1)
+function cubicBezier(t: number, p1x: number, p1y: number, p2x: number, p2y: number): number {
+ const cx: number = 3 * p1x
+ const bx: number = 3 * (p2x - p1x) - cx
+ const ax: number = 1 - cx - bx
+ const cy: number = 3 * p1y
+ const by: number = 3 * (p2y - p1y) - cy
+ const ay: number = 1 - cy - by
+
+ function sampleCurveX(t: number): number {
+ return ((ax * t + bx) * t + cx) * t
+ }
+ function sampleCurveY(t: number): number {
+ return ((ay * t + by) * t + cy) * t
+ }
+ function solveCurveX(x: number): number {
+ let t2: number = x
+ for (let i = 0; i < 8; i++) {
+ const x2: number = sampleCurveX(t2) - x
+ if (Math.abs(x2) < 1e-6) return t2
+ const d2: number = (3 * ax * t2 + 2 * bx) * t2 + cx
+ if (Math.abs(d2) < 1e-6) break
+ t2 -= x2 / d2
+ }
+ return t2
+ }
+ return sampleCurveY(solveCurveX(t))
+}
+
+export function highlightElement(element: HTMLElement | null): void {
+ if (!element) return
+
+ // Get element's bounding rectangle
+ const rect: DOMRect = element.getBoundingClientRect()
+ const padding = 10 // Extra space for shadow effect
+
+ // Create canvas
+ const canvas: HTMLCanvasElement = document.createElement("canvas")
+ canvas.style.position = "absolute"
+ canvas.style.top = `${rect.top + window.scrollY - padding}px`
+ canvas.style.left = `${rect.left + window.scrollX - padding}px`
+ canvas.width = rect.width + padding * 2
+ canvas.height = rect.height + padding * 2
+
+ canvas.style.pointerEvents = "none"
+ document.body.append(canvas)
+
+ const ctx: CanvasRenderingContext2D = canvas.getContext("2d")!
+ if (!ctx) {
+ canvas.remove()
+ return
+ }
+
+ const duration = 1000 // 1000ms
+ const startTime: number = performance.now()
+ const borderRadius = 8 // Border radius for rounded corners
+
+ // Keyframes (mimicking CSS)
+ const keyframes: HighlightKeyframe[] = [
+ {
+ shadowWidth: 4,
+ shadowOpacity: 0.8,
+ outlineWidth: 1,
+ outlineOpacity: 0.5,
+ },
+ {
+ shadowWidth: 2,
+ shadowOpacity: 0.4,
+ outlineWidth: 1,
+ outlineOpacity: 0.3,
+ },
+ {
+ shadowWidth: 0,
+ shadowOpacity: 0,
+ outlineWidth: 0,
+ outlineOpacity: 0,
+ },
+ ]
+
+ function interpolate(start: number, end: number, factor: number): number {
+ return start + (end - start) * factor
+ }
+
+ function animate(): void {
+ const elapsed: number = performance.now() - startTime
+ let t: number = elapsed / duration
+ if (t > 1) t = 1
+
+ // Apply cubic-bezier easing
+ t = cubicBezier(t, 0.4, 0, 0.2, 1)
+
+ // Determine which keyframe segment we're in
+ const segmentDuration: number = 1 / (keyframes.length - 1)
+ const segmentIndex: number = Math.floor(t / segmentDuration)
+ const segmentT: number = (t - segmentIndex * segmentDuration) / segmentDuration
+
+ if (segmentIndex >= keyframes.length - 1) {
+ // Animation complete, remove canvas
+ canvas.remove()
+ return
+ }
+
+ const startFrame: HighlightKeyframe = keyframes[segmentIndex]!
+ const endFrame: HighlightKeyframe = keyframes[segmentIndex + 1]!
+
+ // Interpolate values
+ const shadowWidth: number = interpolate(startFrame.shadowWidth, endFrame.shadowWidth, segmentT)
+ const shadowOpacity: number = interpolate(
+ startFrame.shadowOpacity,
+ endFrame.shadowOpacity,
+ segmentT,
+ )
+ const outlineWidth: number = interpolate(
+ startFrame.outlineWidth,
+ endFrame.outlineWidth,
+ segmentT,
+ )
+ const outlineOpacity: number = interpolate(
+ startFrame.outlineOpacity,
+ endFrame.outlineOpacity,
+ segmentT,
+ )
+
+ // Clear canvas
+ ctx.clearRect(0, 0, canvas.width, canvas.height)
+
+ // Draw shadow (approximated as thick border with rounded corners)
+ if (shadowWidth > 0) {
+ ctx.strokeStyle = `rgba(255, 165, 0, ${shadowOpacity})` // Orange color
+ ctx.lineWidth = shadowWidth
+ ctx.beginPath()
+ ctx.roundRect(
+ padding - shadowWidth / 2,
+ padding - shadowWidth / 2,
+ rect.width + shadowWidth,
+ rect.height + shadowWidth,
+ borderRadius,
+ )
+ ctx.stroke()
+ }
+
+ // Draw outline (with rounded corners)
+ if (outlineWidth > 0) {
+ ctx.strokeStyle = `rgba(255, 165, 0, ${outlineOpacity})`
+ ctx.lineWidth = outlineWidth
+ ctx.beginPath()
+ ctx.roundRect(
+ padding - outlineWidth / 2,
+ padding - outlineWidth / 2,
+ rect.width + outlineWidth,
+ rect.height + outlineWidth,
+ borderRadius,
+ )
+ ctx.stroke()
+ }
+
+ if (t < 1) {
+ requestAnimationFrame(animate)
+ } else {
+ canvas.remove()
+ }
+ }
+
+ requestAnimationFrame(animate)
+}
diff --git a/packages/internal/components/src/ui/button/action-button.tsx b/packages/internal/components/src/ui/button/action-button.tsx
index 853f21a32..7ae2f4faf 100644
--- a/packages/internal/components/src/ui/button/action-button.tsx
+++ b/packages/internal/components/src/ui/button/action-button.tsx
@@ -1,4 +1,4 @@
-import { useFocusable } from "@follow/components/common/Focusable.jsx"
+import { useFocusable } from "@follow/components/common/Focusable/index.js"
import { stopPropagation } from "@follow/utils/dom"
import { cn, getOS } from "@follow/utils/utils"
import * as React from "react"