mirror of
https://github.com/linsa-io/linsa.git
synced 2026-04-19 23:11:30 +02:00
Move to TanStack Start from Next.js (#184)
This commit is contained in:
115
web/shared/la-editor/components/bubble-menu/bubble-menu.tsx
Normal file
115
web/shared/la-editor/components/bubble-menu/bubble-menu.tsx
Normal file
@@ -0,0 +1,115 @@
|
||||
import { useTextmenuCommands } from "../../hooks/use-text-menu-commands"
|
||||
import { PopoverWrapper } from "../ui/popover-wrapper"
|
||||
import { useTextmenuStates } from "../../hooks/use-text-menu-states"
|
||||
import { BubbleMenu as TiptapBubbleMenu, Editor } from "@tiptap/react"
|
||||
import { ToolbarButton } from "../ui/toolbar-button"
|
||||
import { Icon } from "../ui/icon"
|
||||
import { Keybind } from "../ui/keybind"
|
||||
|
||||
export type BubbleMenuProps = {
|
||||
editor: Editor
|
||||
}
|
||||
|
||||
export const BubbleMenu = ({ editor }: BubbleMenuProps) => {
|
||||
const commands = useTextmenuCommands(editor)
|
||||
const states = useTextmenuStates(editor)
|
||||
|
||||
const toolbarButtonClassname =
|
||||
"hover:opacity-100 transition-all dark:border-slate-500/10 border-gray-400 hover:border-b-2 active:translate-y-0 hover:translate-y-[-1.5px] hover:bg-zinc-300 dark:hover:bg-neutral-800 shadow-md rounded-[10px]"
|
||||
|
||||
return (
|
||||
<TiptapBubbleMenu
|
||||
tippyOptions={{
|
||||
// duration: [0, 999999],
|
||||
popperOptions: { placement: "top-start" },
|
||||
}}
|
||||
className="flex h-[40px] min-h-[40px] items-center rounded-[14px] shadow-md"
|
||||
editor={editor}
|
||||
pluginKey="textMenu"
|
||||
shouldShow={states.shouldShow}
|
||||
updateDelay={100}
|
||||
>
|
||||
<PopoverWrapper
|
||||
className="flex items-center rounded-[14px] border border-slate-400/10 bg-gray-100 p-[4px] dark:bg-[#121212]"
|
||||
style={{
|
||||
boxShadow: "inset 0px 0px 5px 3px var(--boxShadow)",
|
||||
}}
|
||||
>
|
||||
<div className="flex space-x-1">
|
||||
<Keybind keys={["Ctrl", "I"]}>
|
||||
<ToolbarButton
|
||||
className={toolbarButtonClassname}
|
||||
value="bold"
|
||||
aria-label="Bold"
|
||||
onPressedChange={commands.onBold}
|
||||
isActive={states.isBold}
|
||||
>
|
||||
<Icon name="Bold" strokeWidth={2.5} />
|
||||
</ToolbarButton>
|
||||
</Keybind>
|
||||
|
||||
<Keybind keys={["Ctrl", "U"]}>
|
||||
<ToolbarButton
|
||||
className={toolbarButtonClassname}
|
||||
value="italic"
|
||||
aria-label="Italic"
|
||||
onClick={commands.onItalic}
|
||||
isActive={states.isItalic}
|
||||
>
|
||||
<Icon name="Italic" strokeWidth={2.5} />
|
||||
</ToolbarButton>
|
||||
</Keybind>
|
||||
<Keybind keys={["Ctrl", "S"]}>
|
||||
<ToolbarButton
|
||||
className={toolbarButtonClassname}
|
||||
value="strikethrough"
|
||||
aria-label="Strikethrough"
|
||||
onClick={commands.onStrike}
|
||||
isActive={states.isStrike}
|
||||
>
|
||||
<Icon name="Strikethrough" strokeWidth={2.5} />
|
||||
</ToolbarButton>
|
||||
</Keybind>
|
||||
{/* <ToolbarButton value="link" aria-label="Link">
|
||||
<Icon name="Link" strokeWidth={2.5} />
|
||||
</ToolbarButton> */}
|
||||
<Keybind keys={["cmd", "K"]}>
|
||||
<ToolbarButton
|
||||
className={toolbarButtonClassname}
|
||||
value="quote"
|
||||
aria-label="Quote"
|
||||
onClick={commands.onCode}
|
||||
isActive={states.isCode}
|
||||
>
|
||||
<Icon name="Quote" strokeWidth={2.5} />
|
||||
</ToolbarButton>
|
||||
</Keybind>
|
||||
<Keybind keys={["Ctrl", "O"]}>
|
||||
<ToolbarButton
|
||||
className={toolbarButtonClassname}
|
||||
value="inline code"
|
||||
aria-label="Inline code"
|
||||
onClick={commands.onCode}
|
||||
isActive={states.isCode}
|
||||
>
|
||||
<Icon name="Braces" strokeWidth={2.5} />
|
||||
</ToolbarButton>
|
||||
</Keybind>
|
||||
<ToolbarButton
|
||||
className={toolbarButtonClassname}
|
||||
value="code block"
|
||||
aria-label="Code block"
|
||||
onClick={commands.onCodeBlock}
|
||||
>
|
||||
<Icon name="Code" strokeWidth={2.5} />
|
||||
</ToolbarButton>
|
||||
{/* <ToolbarButton value="list" aria-label="List">
|
||||
<Icon name="List" strokeWidth={2.5} />
|
||||
</ToolbarButton> */}
|
||||
</div>
|
||||
</PopoverWrapper>
|
||||
</TiptapBubbleMenu>
|
||||
)
|
||||
}
|
||||
|
||||
export default BubbleMenu
|
||||
1
web/shared/la-editor/components/bubble-menu/index.ts
Normal file
1
web/shared/la-editor/components/bubble-menu/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./bubble-menu"
|
||||
30
web/shared/la-editor/components/ui/icon.tsx
Normal file
30
web/shared/la-editor/components/ui/icon.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
import * as React from "react"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { icons } from "lucide-react"
|
||||
|
||||
export type IconProps = {
|
||||
name: keyof typeof icons
|
||||
className?: string
|
||||
strokeWidth?: number
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
export const Icon = React.memo(
|
||||
({ name, className, size, strokeWidth, ...props }: IconProps) => {
|
||||
const IconComponent = icons[name]
|
||||
|
||||
if (!IconComponent) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<IconComponent
|
||||
className={cn(!size ? "size-4" : size, className)}
|
||||
strokeWidth={strokeWidth || 2}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
Icon.displayName = "Icon"
|
||||
52
web/shared/la-editor/components/ui/keybind.tsx
Normal file
52
web/shared/la-editor/components/ui/keybind.tsx
Normal file
@@ -0,0 +1,52 @@
|
||||
import { ReactNode, useState } from "react"
|
||||
import { motion } from "framer-motion"
|
||||
|
||||
export function Keybind({
|
||||
keys,
|
||||
children,
|
||||
}: {
|
||||
keys: string[]
|
||||
children: ReactNode
|
||||
}) {
|
||||
const [hovered, setHovered] = useState(false)
|
||||
const variants = {
|
||||
hidden: { opacity: 0, y: 6, x: "-50%" },
|
||||
visible: { opacity: 1, y: 0, x: "-50%" },
|
||||
}
|
||||
return (
|
||||
<div
|
||||
onMouseEnter={() => setHovered(true)}
|
||||
onMouseLeave={() => setHovered(false)}
|
||||
className="group relative h-full"
|
||||
>
|
||||
<motion.div
|
||||
initial="hidden"
|
||||
animate={hovered ? "visible" : "hidden"}
|
||||
variants={variants}
|
||||
transition={{ duration: 0.2, delay: 0.4 }}
|
||||
className="absolute left-[50%] top-[-30px] flex h-fit w-fit items-center rounded-[7px] border border-slate-400/30 bg-gray-100 p-[3px] px-2 text-[10px] drop-shadow-sm dark:border-slate-400/10 dark:bg-[#191919]"
|
||||
style={{
|
||||
boxShadow: "inset 0px 0px 6px 2px var(--boxShadow)",
|
||||
}}
|
||||
>
|
||||
{keys.map((key, index) => (
|
||||
<span key={key}>
|
||||
{index > 0 && <span className="mx-1">+</span>}
|
||||
{(() => {
|
||||
switch (key.toLowerCase()) {
|
||||
case "cmd":
|
||||
return "⌘"
|
||||
case "shift":
|
||||
return "⇪"
|
||||
|
||||
default:
|
||||
return key
|
||||
}
|
||||
})()}
|
||||
</span>
|
||||
))}
|
||||
</motion.div>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
24
web/shared/la-editor/components/ui/popover-wrapper.tsx
Normal file
24
web/shared/la-editor/components/ui/popover-wrapper.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
import * as React from "react"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
export type PopoverWrapperProps = React.HTMLProps<HTMLDivElement>
|
||||
|
||||
export const PopoverWrapper = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
PopoverWrapperProps
|
||||
>(({ children, className, ...props }, ref) => {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"bg-popover text-popover-foreground rounded-lg border shadow-sm",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
ref={ref}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
PopoverWrapper.displayName = "PopoverWrapper"
|
||||
55
web/shared/la-editor/components/ui/shortcut.tsx
Normal file
55
web/shared/la-editor/components/ui/shortcut.tsx
Normal file
@@ -0,0 +1,55 @@
|
||||
import * as React from "react"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { getShortcutKey } from "@/lib/utils"
|
||||
|
||||
export interface ShortcutKeyWrapperProps
|
||||
extends React.HTMLAttributes<HTMLSpanElement> {
|
||||
ariaLabel: string
|
||||
}
|
||||
|
||||
const ShortcutKeyWrapper = React.forwardRef<
|
||||
HTMLSpanElement,
|
||||
ShortcutKeyWrapperProps
|
||||
>(({ className, ariaLabel, children, ...props }, ref) => {
|
||||
return (
|
||||
<span
|
||||
aria-label={ariaLabel}
|
||||
className={cn("inline-flex items-center gap-0.5", className)}
|
||||
{...props}
|
||||
ref={ref}
|
||||
>
|
||||
{children}
|
||||
</span>
|
||||
)
|
||||
})
|
||||
|
||||
ShortcutKeyWrapper.displayName = "ShortcutKeyWrapper"
|
||||
|
||||
export interface ShortcutKeyProps
|
||||
extends React.HTMLAttributes<HTMLSpanElement> {
|
||||
shortcut: string
|
||||
}
|
||||
|
||||
const ShortcutKey = React.forwardRef<HTMLSpanElement, ShortcutKeyProps>(
|
||||
({ className, shortcut, ...props }, ref) => {
|
||||
return (
|
||||
<kbd
|
||||
className={cn(
|
||||
"inline-block min-w-2.5 text-center align-baseline font-sans text-xs font-medium capitalize text-[rgb(156,157,160)]",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
ref={ref}
|
||||
>
|
||||
{getShortcutKey(shortcut).symbol}
|
||||
</kbd>
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
ShortcutKey.displayName = "ShortcutKey"
|
||||
|
||||
export const Shortcut = {
|
||||
Wrapper: ShortcutKeyWrapper,
|
||||
Key: ShortcutKey,
|
||||
}
|
||||
57
web/shared/la-editor/components/ui/toolbar-button.tsx
Normal file
57
web/shared/la-editor/components/ui/toolbar-button.tsx
Normal file
@@ -0,0 +1,57 @@
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip"
|
||||
import { Toggle } from "@/components/ui/toggle"
|
||||
|
||||
import * as React from "react"
|
||||
import { cn } from "@/lib/utils"
|
||||
import type { TooltipContentProps } from "@radix-ui/react-tooltip"
|
||||
|
||||
interface ToolbarButtonProps
|
||||
extends React.ComponentPropsWithoutRef<typeof Toggle> {
|
||||
isActive?: boolean
|
||||
tooltip?: string
|
||||
tooltipOptions?: TooltipContentProps
|
||||
}
|
||||
|
||||
const ToolbarButton = React.forwardRef<HTMLButtonElement, ToolbarButtonProps>(
|
||||
function ToolbarButton(
|
||||
{ isActive, children, tooltip, className, tooltipOptions, ...props },
|
||||
ref,
|
||||
) {
|
||||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Toggle
|
||||
size="sm"
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"size-7 rounded-md p-0",
|
||||
{
|
||||
"bg-primary/10 text-primary hover:bg-primary/10 hover:text-primary":
|
||||
isActive,
|
||||
},
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</Toggle>
|
||||
</TooltipTrigger>
|
||||
{tooltip && (
|
||||
<TooltipContent {...tooltipOptions}>
|
||||
<div className="flex flex-col items-center text-center">
|
||||
{tooltip}
|
||||
</div>
|
||||
</TooltipContent>
|
||||
)}
|
||||
</Tooltip>
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
ToolbarButton.displayName = "ToolbarButton"
|
||||
|
||||
export { ToolbarButton }
|
||||
Reference in New Issue
Block a user