feat(shortcut): Keyboard Navigation (#168)

* chore: remove sliding menu

* feat(ui): sheet

* feat: shortcut component

* chore: register new shortcut component to layout

* fix: react attr naming

* fix: set default to false for shortcut

* feat(store): keydown-manager

* feat(hooks): keyboard manager

* chore: use util from base for la-editor

* chore: use util from base for minimal-tiptap-editor

* chore(utils): keyboard

* chore: use new keyboard manager

* fix: uniqueness of certain component

* feat: global key handler

* chore: implement new key handler
This commit is contained in:
Aslam
2024-09-19 21:17:11 +07:00
committed by GitHub
parent 0df105f186
commit 8eed3f8cc2
23 changed files with 686 additions and 515 deletions

109
web/components/ui/sheet.tsx Normal file
View File

@@ -0,0 +1,109 @@
"use client"
import * as React from "react"
import * as SheetPrimitive from "@radix-ui/react-dialog"
import { Cross2Icon } from "@radix-ui/react-icons"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const Sheet = SheetPrimitive.Root
const SheetTrigger = SheetPrimitive.Trigger
const SheetClose = SheetPrimitive.Close
const SheetPortal = SheetPrimitive.Portal
const SheetOverlay = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<SheetPrimitive.Overlay
className={cn(
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/80",
className
)}
{...props}
ref={ref}
/>
))
SheetOverlay.displayName = SheetPrimitive.Overlay.displayName
export const sheetVariants = cva(
"fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500 data-[state=open]:animate-in data-[state=closed]:animate-out",
{
variants: {
side: {
top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
bottom:
"inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm",
right:
"inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm"
}
},
defaultVariants: {
side: "right"
}
}
)
interface SheetContentProps
extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,
VariantProps<typeof sheetVariants> {}
const SheetContent = React.forwardRef<React.ElementRef<typeof SheetPrimitive.Content>, SheetContentProps>(
({ side = "right", className, children, ...props }, ref) => (
<SheetPortal>
<SheetOverlay />
<SheetPrimitive.Content ref={ref} className={cn(sheetVariants({ side }), className)} {...props}>
<SheetPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-secondary absolute right-4 top-4 rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:pointer-events-none">
<Cross2Icon className="h-4 w-4" />
<span className="sr-only">Close</span>
</SheetPrimitive.Close>
{children}
</SheetPrimitive.Content>
</SheetPortal>
)
)
SheetContent.displayName = SheetPrimitive.Content.displayName
const SheetHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
<div className={cn("flex flex-col space-y-2 text-center sm:text-left", className)} {...props} />
)
SheetHeader.displayName = "SheetHeader"
const SheetFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
<div className={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)} {...props} />
)
SheetFooter.displayName = "SheetFooter"
const SheetTitle = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Title>
>(({ className, ...props }, ref) => (
<SheetPrimitive.Title ref={ref} className={cn("text-foreground text-lg font-semibold", className)} {...props} />
))
SheetTitle.displayName = SheetPrimitive.Title.displayName
const SheetDescription = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Description>
>(({ className, ...props }, ref) => (
<SheetPrimitive.Description ref={ref} className={cn("text-muted-foreground text-sm", className)} {...props} />
))
SheetDescription.displayName = SheetPrimitive.Description.displayName
export {
Sheet,
SheetPortal,
SheetOverlay,
SheetTrigger,
SheetClose,
SheetContent,
SheetHeader,
SheetFooter,
SheetTitle,
SheetDescription
}

View File

@@ -1,82 +0,0 @@
import { XIcon } from "lucide-react"
import { useState, useEffect, useRef } from "react"
import { motion, AnimatePresence } from "framer-motion"
import { showHotkeyPanelAtom } from "@/store/sidebar"
import { useAtom } from "jotai/react"
export default function SlidingMenu() {
const [isOpen, setIsOpen] = useAtom(showHotkeyPanelAtom)
const panelRef = useRef<HTMLDivElement>(null)
const [shortcuts] = useState<{ name: string; shortcut: string[] }[]>([
// TODO: change to better keybind
// TODO: windows users don't understand these symbols, figure out better way to show keybinds
{ name: "New Todo", shortcut: ["⌘", "⌃", "n"] },
{ name: "CMD Palette", shortcut: ["⌘", "k"] }
// TODO: add
// { name: "Global Search", shortcut: ["."] },
// { name: "(/pages)", shortcut: [".", "."] }
])
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (panelRef.current && !panelRef.current.contains(event.target as Node)) {
setIsOpen(false)
}
}
if (isOpen) {
document.addEventListener("mousedown", handleClickOutside)
}
return () => {
document.removeEventListener("mousedown", handleClickOutside)
}
}, [isOpen, setIsOpen])
return (
<AnimatePresence>
{isOpen && (
<>
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.2 }}
className="fixed inset-0 z-[99] bg-black bg-opacity-50"
onClick={() => setIsOpen(false)}
/>
<motion.div
ref={panelRef}
initial={{ x: "100%" }}
animate={{ x: 0 }}
exit={{ x: "100%", transition: { duration: 0.1, ease: "easeIn" } }}
transition={{ type: "spring", stiffness: 300, damping: 30 }}
className="fixed right-0 top-0 z-[100] h-full p-4"
>
<div className="flex h-full w-[300px] flex-col gap-4 rounded-lg border border-slate-400/20 bg-white p-3 pl-4 drop-shadow-md dark:bg-neutral-950">
<div className="flex flex-row items-center justify-between gap-4">
<div className="">Shortcuts</div>
<button
onClick={() => setIsOpen(false)}
className="flex h-[28px] w-[28px] items-center justify-center rounded-md border border-slate-400/20 text-black/60 dark:text-white/60"
>
<XIcon className="h-[16px] w-[16px]" />
</button>
</div>
<div className="flex flex-col gap-1 text-[12px]">
{shortcuts.map((shortcut, index) => (
<div key={index} className="flex flex-row items-center justify-between gap-4">
<div className="opacity-40">{shortcut.name}</div>
<div className="flex min-w-[20px] items-center justify-center rounded-sm bg-gray-100 p-1 px-2 dark:bg-neutral-900">
{shortcut.shortcut.join(" ")}
</div>
</div>
))}
</div>
</div>
</motion.div>
</>
)}
</AnimatePresence>
)
}