mirror of
https://github.com/linsa-io/linsa.git
synced 2026-01-12 12:20:23 +01:00
* chore: remove useKeyDownListener * chore: remove react-use, update jazz version and add query string * chore: update jazz version * chore: use simple mac or win utils code * feat(util): add isTextInput * feat(hooks): all needed hooks * fix: link bunch stuff * fix: page bunch stuff * chore: bunch update for custom component * chore: use throttle from internal hook * chore: topic bunch stuff * chore: update layout * fix: truncate content header of topic detail
165 lines
4.9 KiB
TypeScript
165 lines
4.9 KiB
TypeScript
"use client"
|
|
|
|
import * as React from "react"
|
|
import * as SheetPrimitive from "@radix-ui/react-dialog"
|
|
import { atom, useAtom } from "jotai"
|
|
import { Sheet, SheetPortal, SheetOverlay, SheetTitle, sheetVariants, SheetDescription } from "@/components/ui/sheet"
|
|
import { LaIcon } from "../la-icon"
|
|
import { cn } from "@/lib/utils"
|
|
import { buttonVariants } from "@/components/ui/button"
|
|
import { Input } from "@/components/ui/input"
|
|
import { useKeyboardManager } from "@/hooks/use-keyboard-manager"
|
|
|
|
export const showShortcutAtom = atom(false)
|
|
|
|
type ShortcutItem = {
|
|
label: string
|
|
keys: string[]
|
|
then?: string[]
|
|
}
|
|
|
|
type ShortcutSection = {
|
|
title: string
|
|
shortcuts: ShortcutItem[]
|
|
}
|
|
|
|
const SHORTCUTS: ShortcutSection[] = [
|
|
{
|
|
title: "General",
|
|
shortcuts: [
|
|
{ label: "Open command menu", keys: ["⌘", "k"] },
|
|
{ label: "Log out", keys: ["⌥", "⇧", "q"] }
|
|
]
|
|
},
|
|
{
|
|
title: "Navigation",
|
|
shortcuts: [
|
|
{ label: "Go to link", keys: ["G"], then: ["L"] },
|
|
{ label: "Go to page", keys: ["G"], then: ["P"] },
|
|
{ label: "Go to topic", keys: ["G"], then: ["T"] }
|
|
]
|
|
},
|
|
{
|
|
title: "Links",
|
|
shortcuts: [{ label: "Create new link", keys: ["c"] }]
|
|
},
|
|
{
|
|
title: "Pages",
|
|
shortcuts: [{ label: "Create new page", keys: ["p"] }]
|
|
}
|
|
]
|
|
|
|
const ShortcutKey: React.FC<{ keyChar: string }> = ({ keyChar }) => (
|
|
<kbd
|
|
aria-hidden="true"
|
|
className="inline-flex size-5 items-center justify-center rounded border font-sans text-xs capitalize"
|
|
>
|
|
{keyChar}
|
|
</kbd>
|
|
)
|
|
|
|
const ShortcutItem: React.FC<ShortcutItem> = ({ label, keys, then }) => (
|
|
<div className="flex flex-row items-center gap-2">
|
|
<dt className="flex grow items-center">
|
|
<span className="text-muted-foreground text-left text-sm">{label}</span>
|
|
</dt>
|
|
<dd className="flex items-end">
|
|
<span className="text-left">
|
|
<span
|
|
aria-label={keys.join(" ") + (then ? ` then ${then.join(" ")}` : "")}
|
|
className="inline-flex items-center gap-1"
|
|
>
|
|
{keys.map((key, index) => (
|
|
<ShortcutKey key={index} keyChar={key} />
|
|
))}
|
|
{then && (
|
|
<>
|
|
<span className="text-muted-foreground text-xs">then</span>
|
|
{then.map((key, index) => (
|
|
<ShortcutKey key={`then-${index}`} keyChar={key} />
|
|
))}
|
|
</>
|
|
)}
|
|
</span>
|
|
</span>
|
|
</dd>
|
|
</div>
|
|
)
|
|
|
|
const ShortcutSection: React.FC<ShortcutSection> = ({ title, shortcuts }) => (
|
|
<section className="flex flex-col gap-2">
|
|
<h2 className="inline-flex gap-1.5 text-sm">{title}</h2>
|
|
<dl className="m-0 flex flex-col gap-2">
|
|
{shortcuts.map((shortcut, index) => (
|
|
<ShortcutItem key={index} {...shortcut} />
|
|
))}
|
|
</dl>
|
|
</section>
|
|
)
|
|
|
|
export function Shortcut() {
|
|
const [showShortcut, setShowShortcut] = useAtom(showShortcutAtom)
|
|
const [searchQuery, setSearchQuery] = React.useState("")
|
|
|
|
const { disableKeydown } = useKeyboardManager("shortcutSection")
|
|
|
|
React.useEffect(() => {
|
|
disableKeydown(showShortcut)
|
|
}, [showShortcut, disableKeydown])
|
|
|
|
const filteredShortcuts = React.useMemo(() => {
|
|
if (!searchQuery) return SHORTCUTS
|
|
|
|
return SHORTCUTS.map(section => ({
|
|
...section,
|
|
shortcuts: section.shortcuts.filter(shortcut => shortcut.label.toLowerCase().includes(searchQuery.toLowerCase()))
|
|
})).filter(section => section.shortcuts.length > 0)
|
|
}, [searchQuery])
|
|
|
|
return (
|
|
<Sheet open={showShortcut} onOpenChange={setShowShortcut}>
|
|
<SheetPortal>
|
|
<SheetOverlay className="bg-black/10" />
|
|
<SheetPrimitive.Content
|
|
className={cn(sheetVariants({ side: "right" }), "m-3 h-[calc(100vh-24px)] rounded-md p-0")}
|
|
>
|
|
<header className="flex flex-[0_0_auto] items-center gap-3 px-5 pb-4 pt-5">
|
|
<SheetTitle className="text-base font-medium">Keyboard Shortcuts</SheetTitle>
|
|
<SheetDescription className="sr-only">Quickly navigate around the app</SheetDescription>
|
|
|
|
<div className="flex-auto"></div>
|
|
|
|
<SheetPrimitive.Close className={cn(buttonVariants({ size: "icon", variant: "ghost" }), "size-6 p-0")}>
|
|
<LaIcon name="X" className="text-muted-foreground size-5" />
|
|
<span className="sr-only">Close</span>
|
|
</SheetPrimitive.Close>
|
|
</header>
|
|
|
|
<div className="flex flex-col gap-1 px-5 pb-6">
|
|
<form className="relative flex items-center">
|
|
<LaIcon name="Search" className="text-muted-foreground absolute left-3 size-4" />
|
|
<Input
|
|
autoFocus
|
|
placeholder="Search shortcuts"
|
|
className="border-muted-foreground/50 focus-visible:border-muted-foreground h-10 pl-10 focus-visible:ring-0"
|
|
value={searchQuery}
|
|
onChange={e => setSearchQuery(e.target.value)}
|
|
/>
|
|
</form>
|
|
</div>
|
|
|
|
<main className="flex-auto overflow-y-auto overflow-x-hidden [scrollbar-gutter:auto]">
|
|
<div className="px-5 pb-5">
|
|
<div role="region" aria-live="polite" className="flex flex-col gap-7">
|
|
{filteredShortcuts.map((section, index) => (
|
|
<ShortcutSection key={index} {...section} />
|
|
))}
|
|
</div>
|
|
</div>
|
|
</main>
|
|
</SheetPrimitive.Content>
|
|
</SheetPortal>
|
|
</Sheet>
|
|
)
|
|
}
|