-
{children}
diff --git a/web/components/custom/Shortcut/shortcut.tsx b/web/components/custom/Shortcut/shortcut.tsx
new file mode 100644
index 00000000..e4bd5fc6
--- /dev/null
+++ b/web/components/custom/Shortcut/shortcut.tsx
@@ -0,0 +1,155 @@
+"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"] }
+ ]
+ }
+]
+
+const ShortcutKey: React.FC<{ keyChar: string }> = ({ keyChar }) => (
+
+ {keyChar}
+
+)
+
+const ShortcutItem: React.FC
= ({ label, keys, then }) => (
+
+
+ {label}
+
+
+
+
+ {keys.map((key, index) => (
+
+ ))}
+ {then && (
+ <>
+ then
+ {then.map((key, index) => (
+
+ ))}
+ >
+ )}
+
+
+
+
+)
+
+const ShortcutSection: React.FC = ({ title, shortcuts }) => (
+
+ {title}
+
+ {shortcuts.map((shortcut, index) => (
+
+ ))}
+
+
+)
+
+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 (
+
+
+
+
+
+ Keyboard Shortcuts
+ Quickly navigate around the app
+
+
+
+
+
+ Close
+
+
+
+
+
+
+
+
+
+
+ {filteredShortcuts.map((section, index) => (
+
+ ))}
+
+
+
+
+
+
+ )
+}
diff --git a/web/components/custom/command-palette/command-palette.tsx b/web/components/custom/command-palette/command-palette.tsx
index d797e381..39aad7c9 100644
--- a/web/components/custom/command-palette/command-palette.tsx
+++ b/web/components/custom/command-palette/command-palette.tsx
@@ -9,6 +9,7 @@ import { searchSafeRegExp } from "@/lib/utils"
import { GraphNode } from "@/components/routes/public/PublicHomeRoute"
import { useCommandActions } from "./hooks/use-command-actions"
import { atom, useAtom } from "jotai"
+import { useKeydownListener } from "@/hooks/use-keydown-listener"
const graph_data_promise = import("@/components/routes/public/graph-data.json").then(a => a.default)
@@ -29,17 +30,17 @@ export function CommandPalette() {
const raw_graph_data = React.use(graph_data_promise) as GraphNode[]
- React.useEffect(() => {
- const down = (e: KeyboardEvent) => {
+ const handleKeydown = React.useCallback(
+ (e: KeyboardEvent) => {
if (e.key === "k" && (e.metaKey || e.ctrlKey)) {
e.preventDefault()
setOpen(prev => !prev)
}
- }
+ },
+ [setOpen]
+ )
- document.addEventListener("keydown", down)
- return () => document.removeEventListener("keydown", down)
- }, [setOpen])
+ useKeydownListener(handleKeydown)
const bounce = React.useCallback(() => {
if (dialogRef.current) {
@@ -118,11 +119,9 @@ export function CommandPalette() {
if (activePage === "home") {
if (!inputValue) {
- // Only show items from the home object when there's no search input
return commandGroups.home
}
- // When there's a search input, search across all categories
const allGroups = [...Object.values(commandGroups).flat(), personalLinks, personalPages, topics]
return allGroups
@@ -133,7 +132,6 @@ export function CommandPalette() {
.filter(group => group.items.length > 0)
}
- // Handle other active pages (searchLinks, searchPages, etc.)
switch (activePage) {
case "searchLinks":
return [...commandGroups.searchLinks, { items: filterItems(personalLinks.items, searchRegex) }]
diff --git a/web/components/custom/discordIcon.tsx b/web/components/custom/discordIcon.tsx
index 9311a3e0..9d172b4b 100644
--- a/web/components/custom/discordIcon.tsx
+++ b/web/components/custom/discordIcon.tsx
@@ -3,21 +3,21 @@ export const DiscordIcon = () => (
)
diff --git a/web/components/custom/global-keydown-handler.tsx b/web/components/custom/global-keydown-handler.tsx
new file mode 100644
index 00000000..527162cd
--- /dev/null
+++ b/web/components/custom/global-keydown-handler.tsx
@@ -0,0 +1,63 @@
+"use client"
+
+import { useState, useEffect, useCallback } from "react"
+import { useKeydownListener } from "@/hooks/use-keydown-listener"
+import { useAuth } from "@clerk/nextjs"
+import { useRouter } from "next/navigation"
+
+type Sequence = {
+ [key: string]: string
+}
+
+const SEQUENCES: Sequence = {
+ GL: "/links",
+ GP: "/pages",
+ GT: "/topics"
+}
+
+const MAX_SEQUENCE_TIME = 1000
+
+export function GlobalKeydownHandler() {
+ const [sequence, setSequence] = useState([])
+ const { signOut } = useAuth()
+ const router = useRouter()
+
+ const resetSequence = useCallback(() => {
+ setSequence([])
+ }, [])
+
+ const checkSequence = useCallback(() => {
+ const sequenceStr = sequence.join("")
+ const route = SEQUENCES[sequenceStr]
+
+ if (route) {
+ console.log(`Navigating to ${route}...`)
+ router.push(route)
+ resetSequence()
+ }
+ }, [sequence, router, resetSequence])
+
+ useKeydownListener((e: KeyboardEvent) => {
+ // Check for logout shortcut
+ if (e.altKey && e.shiftKey && e.code === "KeyQ") {
+ signOut()
+ return
+ }
+
+ // Key sequence handling
+ const key = e.key.toUpperCase()
+ setSequence(prev => [...prev, key])
+ })
+
+ useEffect(() => {
+ checkSequence()
+
+ const timeoutId = setTimeout(() => {
+ resetSequence()
+ }, MAX_SEQUENCE_TIME)
+
+ return () => clearTimeout(timeoutId)
+ }, [sequence, checkSequence, resetSequence])
+
+ return null
+}
diff --git a/web/components/custom/sidebar/partial/profile-section.tsx b/web/components/custom/sidebar/partial/profile-section.tsx
index 2f768c5d..2396c7ac 100644
--- a/web/components/custom/sidebar/partial/profile-section.tsx
+++ b/web/components/custom/sidebar/partial/profile-section.tsx
@@ -1,7 +1,14 @@
-import { LaIcon } from "@/components/custom/la-icon"
-import { DiscordIcon } from "../../discordIcon"
-import { useState } from "react"
+"use client"
+
+import { useEffect, useState } from "react"
import { SignInButton, useAuth, useUser } from "@clerk/nextjs"
+import { useAtom } from "jotai"
+import Link from "next/link"
+import { usePathname } from "next/navigation"
+import { icons } from "lucide-react"
+
+import { LaIcon } from "@/components/custom/la-icon"
+import { DiscordIcon } from "@/components/custom/discordIcon"
import {
DropdownMenu,
DropdownMenuContent,
@@ -10,17 +17,25 @@ import {
DropdownMenuTrigger
} from "@/components/ui/dropdown-menu"
import { Avatar, AvatarImage } from "@/components/ui/avatar"
-import Link from "next/link"
-import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
-import { usePathname } from "next/navigation"
+import { cn } from "@/lib/utils"
import { Feedback } from "./feedback"
+import { showShortcutAtom } from "@/components/custom/Shortcut/shortcut"
+import { ShortcutKey } from "@/components/minimal-tiptap/components/shortcut-key"
+import { useKeyboardManager } from "@/hooks/use-keyboard-manager"
export const ProfileSection: React.FC = () => {
const { user, isSignedIn } = useUser()
const { signOut } = useAuth()
const [menuOpen, setMenuOpen] = useState(false)
const pathname = usePathname()
+ const [, setShowShortcut] = useAtom(showShortcutAtom)
+
+ const { disableKeydown } = useKeyboardManager("profileSection")
+
+ useEffect(() => {
+ disableKeydown(menuOpen)
+ }, [menuOpen, disableKeydown])
if (!isSignedIn) {
return (
@@ -38,88 +53,104 @@ export const ProfileSection: React.FC = () => {
return (
-
-
-
-
-
-
-
-
-
-
- My profile
-
-
-
-
-
-
-
-
- Onboarding
-
-
-
-
-
-
-
-
-
- Docs
-
-
-
-
-
-
-
-
-
- GitHub
-
-
-
-
-
-
-
-
-
- Discord
-
-
-
-
-
- signOut()}>
-
-
- Log out
-
-
-
-
-
-
+
)
}
+
+interface ProfileDropdownProps {
+ user: any
+ menuOpen: boolean
+ setMenuOpen: (open: boolean) => void
+ signOut: () => void
+ setShowShortcut: (show: boolean) => void
+}
+
+const ProfileDropdown: React.FC = ({ user, menuOpen, setMenuOpen, signOut, setShowShortcut }) => (
+
+
+
+
+
+
+
+
+
+
+)
+
+interface DropdownMenuItemsProps {
+ signOut: () => void
+ setShowShortcut: (show: boolean) => void
+}
+
+const DropdownMenuItems: React.FC = ({ signOut, setShowShortcut }) => (
+ <>
+
+ setShowShortcut(true)}>
+
+ Shortcut
+
+
+
+
+
+
+
+
+
+
+ >
+)
+
+interface MenuLinkProps {
+ href: string
+ icon: keyof typeof icons | React.FC
+ text: string
+ iconClass?: string
+}
+
+const MenuLink: React.FC = ({ href, icon, text, iconClass = "" }) => {
+ const IconComponent = typeof icon === "string" ? icons[icon] : icon
+ return (
+
+
+
+
+ {text}
+
+
+
+ )
+}
+
+export default ProfileSection
diff --git a/web/components/la-editor/components/ui/shortcut.tsx b/web/components/la-editor/components/ui/shortcut.tsx
index 978a2b4a..cdb2f976 100644
--- a/web/components/la-editor/components/ui/shortcut.tsx
+++ b/web/components/la-editor/components/ui/shortcut.tsx
@@ -1,6 +1,6 @@
import * as React from "react"
import { cn } from "@/lib/utils"
-import { getShortcutKey } from "../../lib/utils"
+import { getShortcutKey } from "@/lib/utils"
export interface ShortcutKeyWrapperProps extends React.HTMLAttributes {
ariaLabel: string
@@ -32,7 +32,7 @@ const ShortcutKey = React.forwardRef(({ class
{...props}
ref={ref}
>
- {getShortcutKey(shortcut)}
+ {getShortcutKey(shortcut).symbol}
)
})
diff --git a/web/components/la-editor/extensions/slash-command/menu-list.tsx b/web/components/la-editor/extensions/slash-command/menu-list.tsx
index 10377d18..ebf92265 100644
--- a/web/components/la-editor/extensions/slash-command/menu-list.tsx
+++ b/web/components/la-editor/extensions/slash-command/menu-list.tsx
@@ -6,7 +6,7 @@ import { Button } from "@/components/ui/button"
import { Separator } from "@/components/ui/separator"
import { Command, MenuListProps } from "./types"
-import { getShortcutKeys } from "../../lib/utils"
+import { getShortcutKeys } from "@/lib/utils"
import { Icon } from "../../components/ui/icon"
import { PopoverWrapper } from "../../components/ui/popover-wrapper"
import { Shortcut } from "../../components/ui/shortcut"
@@ -136,7 +136,11 @@ export const MenuList = React.forwardRef((props: MenuListProps, ref) => {
{command.label}
-
+ shortcut.readable)
+ .join(" + ")}
+ >
{command.shortcuts.map(shortcut => (
))}
diff --git a/web/components/la-editor/lib/utils/index.ts b/web/components/la-editor/lib/utils/index.ts
index 885f4fa4..60123d5b 100644
--- a/web/components/la-editor/lib/utils/index.ts
+++ b/web/components/la-editor/lib/utils/index.ts
@@ -8,7 +8,5 @@ export function getOutput(editor: Editor, output: LAEditorProps["output"]) {
return ""
}
-export * from "./keyboard"
-export * from "./platform"
export * from "./isCustomNodeSelected"
export * from "./isTextSelected"
diff --git a/web/components/la-editor/lib/utils/keyboard.ts b/web/components/la-editor/lib/utils/keyboard.ts
deleted file mode 100644
index 09739fc2..00000000
--- a/web/components/la-editor/lib/utils/keyboard.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-import { isMacOS } from "./platform"
-
-export const getShortcutKey = (key: string) => {
- const lowercaseKey = key.toLowerCase()
- const macOS = isMacOS()
-
- switch (lowercaseKey) {
- case "mod":
- return macOS ? "⌘" : "Ctrl"
- case "alt":
- return macOS ? "⌥" : "Alt"
- case "shift":
- return macOS ? "⇧" : "Shift"
- default:
- return key
- }
-}
-
-export const getShortcutKeys = (keys: string | string[], separator: string = "") => {
- const keyArray = Array.isArray(keys) ? keys : keys.split(/\s+/)
- const shortcutKeys = keyArray.map(getShortcutKey)
- return shortcutKeys.join(separator)
-}
-
-export default { getShortcutKey, getShortcutKeys }
diff --git a/web/components/la-editor/lib/utils/platform.ts b/web/components/la-editor/lib/utils/platform.ts
deleted file mode 100644
index 2dffa98a..00000000
--- a/web/components/la-editor/lib/utils/platform.ts
+++ /dev/null
@@ -1,46 +0,0 @@
-export interface NavigatorWithUserAgentData extends Navigator {
- userAgentData?: {
- brands: { brand: string; version: string }[]
- mobile: boolean
- platform: string
- getHighEntropyValues: (hints: string[]) => Promise<{
- platform: string
- platformVersion: string
- uaFullVersion: string
- }>
- }
-}
-
-let isMac: boolean | undefined
-
-const getPlatform = () => {
- const nav = navigator as NavigatorWithUserAgentData
- if (nav.userAgentData) {
- if (nav.userAgentData.platform) {
- return nav.userAgentData.platform
- }
-
- nav.userAgentData
- .getHighEntropyValues(["platform"])
- .then(highEntropyValues => {
- if (highEntropyValues.platform) {
- return highEntropyValues.platform
- }
- })
- .catch(() => {
- return navigator.platform || ""
- })
- }
-
- return navigator.platform || ""
-}
-
-export const isMacOS = () => {
- if (isMac === undefined) {
- isMac = getPlatform().toLowerCase().includes("mac")
- }
-
- return isMac
-}
-
-export default isMacOS
diff --git a/web/components/minimal-tiptap/components/shortcut-key.tsx b/web/components/minimal-tiptap/components/shortcut-key.tsx
index 2691528d..e81bdd88 100644
--- a/web/components/minimal-tiptap/components/shortcut-key.tsx
+++ b/web/components/minimal-tiptap/components/shortcut-key.tsx
@@ -1,33 +1,33 @@
-import * as React from 'react'
-import { cn } from '@/lib/utils'
-import { getShortcutKey } from '../utils'
+import * as React from "react"
+import { cn } from "@/lib/utils"
+import { getShortcutKey } from "@/lib/utils"
export interface ShortcutKeyProps extends React.HTMLAttributes {
- keys: string[]
+ keys: string[]
}
export const ShortcutKey = React.forwardRef(({ className, keys, ...props }, ref) => {
- const modifiedKeys = keys.map(key => getShortcutKey(key))
- const ariaLabel = modifiedKeys.map(shortcut => shortcut.readable).join(' + ')
+ const modifiedKeys = keys.map(key => getShortcutKey(key))
+ const ariaLabel = modifiedKeys.map(shortcut => shortcut.readable).join(" + ")
- return (
-
- {modifiedKeys.map(shortcut => (
-
+ {modifiedKeys.map(shortcut => (
+
- {shortcut.symbol}
-
- ))}
-
- )
+ className
+ )}
+ {...props}
+ ref={ref}
+ >
+ {shortcut.symbol}
+
+ ))}
+
+ )
})
-ShortcutKey.displayName = 'ShortcutKey'
+ShortcutKey.displayName = "ShortcutKey"
diff --git a/web/components/minimal-tiptap/components/toolbar-section.tsx b/web/components/minimal-tiptap/components/toolbar-section.tsx
index e296fd17..9aacc01b 100644
--- a/web/components/minimal-tiptap/components/toolbar-section.tsx
+++ b/web/components/minimal-tiptap/components/toolbar-section.tsx
@@ -1,112 +1,112 @@
-import * as React from 'react'
-import type { Editor } from '@tiptap/react'
-import { cn } from '@/lib/utils'
-import { CaretDownIcon } from '@radix-ui/react-icons'
-import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@/components/ui/dropdown-menu'
-import { ToolbarButton } from './toolbar-button'
-import { ShortcutKey } from './shortcut-key'
-import { getShortcutKey } from '../utils'
-import type { FormatAction } from '../types'
-import type { VariantProps } from 'class-variance-authority'
-import type { toggleVariants } from '@/components/ui/toggle'
+import * as React from "react"
+import type { Editor } from "@tiptap/react"
+import { cn } from "@/lib/utils"
+import { CaretDownIcon } from "@radix-ui/react-icons"
+import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"
+import { ToolbarButton } from "./toolbar-button"
+import { ShortcutKey } from "./shortcut-key"
+import { getShortcutKey } from "@/lib/utils"
+import type { FormatAction } from "../types"
+import type { VariantProps } from "class-variance-authority"
+import type { toggleVariants } from "@/components/ui/toggle"
interface ToolbarSectionProps extends VariantProps {
- editor: Editor
- actions: FormatAction[]
- activeActions?: string[]
- mainActionCount?: number
- dropdownIcon?: React.ReactNode
- dropdownTooltip?: string
- dropdownClassName?: string
+ editor: Editor
+ actions: FormatAction[]
+ activeActions?: string[]
+ mainActionCount?: number
+ dropdownIcon?: React.ReactNode
+ dropdownTooltip?: string
+ dropdownClassName?: string
}
export const ToolbarSection: React.FC = ({
- editor,
- actions,
- activeActions = actions.map(action => action.value),
- mainActionCount = 0,
- dropdownIcon,
- dropdownTooltip = 'More options',
- dropdownClassName = 'w-12',
- size,
- variant
+ editor,
+ actions,
+ activeActions = actions.map(action => action.value),
+ mainActionCount = 0,
+ dropdownIcon,
+ dropdownTooltip = "More options",
+ dropdownClassName = "w-12",
+ size,
+ variant
}) => {
- const { mainActions, dropdownActions } = React.useMemo(() => {
- const sortedActions = actions
- .filter(action => activeActions.includes(action.value))
- .sort((a, b) => activeActions.indexOf(a.value) - activeActions.indexOf(b.value))
+ const { mainActions, dropdownActions } = React.useMemo(() => {
+ const sortedActions = actions
+ .filter(action => activeActions.includes(action.value))
+ .sort((a, b) => activeActions.indexOf(a.value) - activeActions.indexOf(b.value))
- return {
- mainActions: sortedActions.slice(0, mainActionCount),
- dropdownActions: sortedActions.slice(mainActionCount)
- }
- }, [actions, activeActions, mainActionCount])
+ return {
+ mainActions: sortedActions.slice(0, mainActionCount),
+ dropdownActions: sortedActions.slice(mainActionCount)
+ }
+ }, [actions, activeActions, mainActionCount])
- const renderToolbarButton = React.useCallback(
- (action: FormatAction) => (
- action.action(editor)}
- disabled={!action.canExecute(editor)}
- isActive={action.isActive(editor)}
- tooltip={`${action.label} ${action.shortcuts.map(s => getShortcutKey(s).symbol).join(' ')}`}
- aria-label={action.label}
- size={size}
- variant={variant}
- >
- {action.icon}
-
- ),
- [editor, size, variant]
- )
+ const renderToolbarButton = React.useCallback(
+ (action: FormatAction) => (
+ action.action(editor)}
+ disabled={!action.canExecute(editor)}
+ isActive={action.isActive(editor)}
+ tooltip={`${action.label} ${action.shortcuts.map(s => getShortcutKey(s).symbol).join(" ")}`}
+ aria-label={action.label}
+ size={size}
+ variant={variant}
+ >
+ {action.icon}
+
+ ),
+ [editor, size, variant]
+ )
- const renderDropdownMenuItem = React.useCallback(
- (action: FormatAction) => (
- action.action(editor)}
- disabled={!action.canExecute(editor)}
- className={cn('flex flex-row items-center justify-between gap-4', {
- 'bg-accent': action.isActive(editor)
- })}
- aria-label={action.label}
- >
- {action.label}
-
-
- ),
- [editor]
- )
+ const renderDropdownMenuItem = React.useCallback(
+ (action: FormatAction) => (
+ action.action(editor)}
+ disabled={!action.canExecute(editor)}
+ className={cn("flex flex-row items-center justify-between gap-4", {
+ "bg-accent": action.isActive(editor)
+ })}
+ aria-label={action.label}
+ >
+ {action.label}
+
+
+ ),
+ [editor]
+ )
- const isDropdownActive = React.useMemo(
- () => dropdownActions.some(action => action.isActive(editor)),
- [dropdownActions, editor]
- )
+ const isDropdownActive = React.useMemo(
+ () => dropdownActions.some(action => action.isActive(editor)),
+ [dropdownActions, editor]
+ )
- return (
- <>
- {mainActions.map(renderToolbarButton)}
- {dropdownActions.length > 0 && (
-
-
-
- {dropdownIcon || }
-
-
-
- {dropdownActions.map(renderDropdownMenuItem)}
-
-
- )}
- >
- )
+ return (
+ <>
+ {mainActions.map(renderToolbarButton)}
+ {dropdownActions.length > 0 && (
+
+
+
+ {dropdownIcon || }
+
+
+
+ {dropdownActions.map(renderDropdownMenuItem)}
+
+
+ )}
+ >
+ )
}
export default ToolbarSection
diff --git a/web/components/minimal-tiptap/utils.ts b/web/components/minimal-tiptap/utils.ts
index d8772d84..d749a3cc 100644
--- a/web/components/minimal-tiptap/utils.ts
+++ b/web/components/minimal-tiptap/utils.ts
@@ -1,81 +1,14 @@
-import type { Editor } from '@tiptap/core'
-import type { MinimalTiptapProps } from './minimal-tiptap'
+import type { Editor } from "@tiptap/core"
+import type { MinimalTiptapProps } from "./minimal-tiptap"
-let isMac: boolean | undefined
+export function getOutput(editor: Editor, format: MinimalTiptapProps["output"]) {
+ if (format === "json") {
+ return editor.getJSON()
+ }
-interface Navigator {
- userAgentData?: {
- brands: { brand: string; version: string }[]
- mobile: boolean
- platform: string
- getHighEntropyValues: (hints: string[]) => Promise<{
- platform: string
- platformVersion: string
- uaFullVersion: string
- }>
- }
-}
-
-function getPlatform(): string {
- const nav = navigator as Navigator
-
- if (nav.userAgentData) {
- if (nav.userAgentData.platform) {
- return nav.userAgentData.platform
- }
-
- nav.userAgentData.getHighEntropyValues(['platform']).then(highEntropyValues => {
- if (highEntropyValues.platform) {
- return highEntropyValues.platform
- }
- })
- }
-
- if (typeof navigator.platform === 'string') {
- return navigator.platform
- }
-
- return ''
-}
-
-export function isMacOS() {
- if (isMac === undefined) {
- isMac = getPlatform().toLowerCase().includes('mac')
- }
-
- return isMac
-}
-
-interface ShortcutKeyResult {
- symbol: string
- readable: string
-}
-
-export function getShortcutKey(key: string): ShortcutKeyResult {
- const lowercaseKey = key.toLowerCase()
- if (lowercaseKey === 'mod') {
- return isMacOS() ? { symbol: '⌘', readable: 'Command' } : { symbol: 'Ctrl', readable: 'Control' }
- } else if (lowercaseKey === 'alt') {
- return isMacOS() ? { symbol: '⌥', readable: 'Option' } : { symbol: 'Alt', readable: 'Alt' }
- } else if (lowercaseKey === 'shift') {
- return isMacOS() ? { symbol: '⇧', readable: 'Shift' } : { symbol: 'Shift', readable: 'Shift' }
- } else {
- return { symbol: key, readable: key }
- }
-}
-
-export function getShortcutKeys(keys: string[]): ShortcutKeyResult[] {
- return keys.map(key => getShortcutKey(key))
-}
-
-export function getOutput(editor: Editor, format: MinimalTiptapProps['output']) {
- if (format === 'json') {
- return editor.getJSON()
- }
-
- if (format === 'html') {
- return editor.getText() ? editor.getHTML() : ''
- }
-
- return editor.getText()
+ if (format === "html") {
+ return editor.getText() ? editor.getHTML() : ""
+ }
+
+ return editor.getText()
}
diff --git a/web/components/routes/link/bottom-bar.tsx b/web/components/routes/link/bottom-bar.tsx
index a9e8eae3..df2165ab 100644
--- a/web/components/routes/link/bottom-bar.tsx
+++ b/web/components/routes/link/bottom-bar.tsx
@@ -1,9 +1,11 @@
+"use client"
+
import React, { useCallback, useEffect, useRef } from "react"
import { motion, AnimatePresence } from "framer-motion"
-import { icons, ZapIcon } from "lucide-react"
+import type { icons } from "lucide-react"
import { Button } from "@/components/ui/button"
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"
-import { getSpecialShortcut, formatShortcut, isMacOS, cn } from "@/lib/utils"
+import { cn, getShortcutKeys } from "@/lib/utils"
import { LaIcon } from "@/components/custom/la-icon"
import { useAtom } from "jotai"
import { parseAsBoolean, useQueryState } from "nuqs"
@@ -13,7 +15,7 @@ import { PersonalLink } from "@/lib/schema"
import { ID } from "jazz-tools"
import { globalLinkFormExceptionRefsAtom } from "./partials/form/link-form"
import { useLinkActions } from "./hooks/use-link-actions"
-import { showHotkeyPanelAtom } from "@/store/sidebar"
+import { useKeydownListener } from "@/hooks/use-keydown-listener"
interface ToolbarButtonProps extends React.ComponentPropsWithoutRef {
icon: keyof typeof icons
@@ -73,8 +75,6 @@ export const LinkBottomBar: React.FC = () => {
}, 100)
}, [setEditId, setCreateMode])
- const [, setShowHotkeyPanel] = useAtom(showHotkeyPanelAtom)
-
useEffect(() => {
setGlobalLinkFormExceptionRefsAtom([
overlayRef,
@@ -119,24 +119,21 @@ export const LinkBottomBar: React.FC = () => {
}
}
- useEffect(() => {
- const handleKeyDown = (event: KeyboardEvent) => {
- const isCreateShortcut = isMacOS()
- ? event.ctrlKey && event.metaKey && event.key.toLowerCase() === "n"
- : event.ctrlKey && event.key.toLowerCase() === "n" && (event.metaKey || event.altKey)
+ const handleKeydown = useCallback(
+ (event: KeyboardEvent) => {
+ const isCreateShortcut = event.key === "c"
if (isCreateShortcut) {
event.preventDefault()
handleCreateMode()
}
- }
+ },
+ [handleCreateMode]
+ )
- window.addEventListener("keydown", handleKeyDown)
- return () => window.removeEventListener("keydown", handleKeyDown)
- }, [handleCreateMode])
+ useKeydownListener(handleKeydown)
- const shortcutKeys = getSpecialShortcut("expandToolbar")
- const shortcutText = formatShortcut(shortcutKeys)
+ const shortcutText = getShortcutKeys(["c"])
return (
{
s.symbol).join("")})`}
ref={plusBtnRef}
/>
)}
- {/* */}
)}
-
- {
- setShowHotkeyPanel(true)
- }}
- />
-
)
}
diff --git a/web/components/routes/topics/detail/TopicDetailRoute.tsx b/web/components/routes/topics/detail/TopicDetailRoute.tsx
index e805fcc8..310ae004 100644
--- a/web/components/routes/topics/detail/TopicDetailRoute.tsx
+++ b/web/components/routes/topics/detail/TopicDetailRoute.tsx
@@ -22,6 +22,7 @@ export function TopicDetailRoute({ topicName }: TopicDetailRouteProps) {
const raw_graph_data = React.use(graph_data_promise) as GraphNode[]
const { me } = useAccountOrGuest({ root: { personalLinks: [] } })
+
const topicID = useMemo(() => me && Topic.findUnique({ topicName }, JAZZ_GLOBAL_GROUP_ID, me), [topicName, me])
const topic = useCoState(Topic, topicID, { latestGlobalGuide: { sections: [] } })
const [activeIndex, setActiveIndex] = useState(-1)
diff --git a/web/components/ui/sheet.tsx b/web/components/ui/sheet.tsx
new file mode 100644
index 00000000..3b5c5dc7
--- /dev/null
+++ b/web/components/ui/sheet.tsx
@@ -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,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, 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,
+ VariantProps {}
+
+const SheetContent = React.forwardRef, SheetContentProps>(
+ ({ side = "right", className, children, ...props }, ref) => (
+
+
+
+
+
+ Close
+
+ {children}
+
+
+ )
+)
+SheetContent.displayName = SheetPrimitive.Content.displayName
+
+const SheetHeader = ({ className, ...props }: React.HTMLAttributes) => (
+
+)
+SheetHeader.displayName = "SheetHeader"
+
+const SheetFooter = ({ className, ...props }: React.HTMLAttributes) => (
+
+)
+SheetFooter.displayName = "SheetFooter"
+
+const SheetTitle = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+SheetTitle.displayName = SheetPrimitive.Title.displayName
+
+const SheetDescription = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+SheetDescription.displayName = SheetPrimitive.Description.displayName
+
+export {
+ Sheet,
+ SheetPortal,
+ SheetOverlay,
+ SheetTrigger,
+ SheetClose,
+ SheetContent,
+ SheetHeader,
+ SheetFooter,
+ SheetTitle,
+ SheetDescription
+}
diff --git a/web/components/ui/sliding-menu.tsx b/web/components/ui/sliding-menu.tsx
deleted file mode 100644
index 1381d753..00000000
--- a/web/components/ui/sliding-menu.tsx
+++ /dev/null
@@ -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(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 (
-
- {isOpen && (
- <>
- setIsOpen(false)}
- />
-
-
-
-
Shortcuts
-
-
-
- {shortcuts.map((shortcut, index) => (
-
-
{shortcut.name}
-
- {shortcut.shortcut.join(" ")}
-
-
- ))}
-
-
-
- >
- )}
-
- )
-}
diff --git a/web/hooks/use-keyboard-manager.ts b/web/hooks/use-keyboard-manager.ts
new file mode 100644
index 00000000..bd8c34de
--- /dev/null
+++ b/web/hooks/use-keyboard-manager.ts
@@ -0,0 +1,38 @@
+import { useAtom } from "jotai"
+import { useEffect, useCallback } from "react"
+import { keyboardDisableSourcesAtom } from "@/store/keydown-manager"
+
+export function useKeyboardManager(sourceId: string) {
+ const [disableSources, setDisableSources] = useAtom(keyboardDisableSourcesAtom)
+
+ useEffect(() => {
+ const handleKeyDown = (event: KeyboardEvent) => {
+ if (disableSources.size > 0) {
+ event.preventDefault()
+ }
+ }
+
+ window.addEventListener("keydown", handleKeyDown)
+ return () => window.removeEventListener("keydown", handleKeyDown)
+ }, [disableSources])
+
+ const disableKeydown = useCallback(
+ (disable: boolean) => {
+ console.log(`${sourceId} disable:`, disable)
+ setDisableSources(prev => {
+ const next = new Set(prev)
+ if (disable) {
+ next.add(sourceId)
+ } else {
+ next.delete(sourceId)
+ }
+ return next
+ })
+ },
+ [setDisableSources, sourceId]
+ )
+
+ const isKeyboardDisabled = disableSources.size > 0
+
+ return { disableKeydown, isKeyboardDisabled }
+}
diff --git a/web/hooks/use-keydown-listener.ts b/web/hooks/use-keydown-listener.ts
new file mode 100644
index 00000000..888f140e
--- /dev/null
+++ b/web/hooks/use-keydown-listener.ts
@@ -0,0 +1,21 @@
+import { useAtomValue } from "jotai"
+import { useEffect, useCallback } from "react"
+import { keyboardDisableSourcesAtom } from "@/store/keydown-manager"
+
+export function useKeydownListener(callback: (event: KeyboardEvent) => void) {
+ const disableSources = useAtomValue(keyboardDisableSourcesAtom)
+
+ const handleKeyDown = useCallback(
+ (event: KeyboardEvent) => {
+ if (disableSources.size === 0) {
+ callback(event)
+ }
+ },
+ [disableSources, callback]
+ )
+
+ useEffect(() => {
+ window.addEventListener("keydown", handleKeyDown)
+ return () => window.removeEventListener("keydown", handleKeyDown)
+ }, [handleKeyDown])
+}
diff --git a/web/lib/utils/keyboard.ts b/web/lib/utils/keyboard.ts
index faaaf7ce..50255db7 100644
--- a/web/lib/utils/keyboard.ts
+++ b/web/lib/utils/keyboard.ts
@@ -55,11 +55,7 @@ export function getShortcutKey(key: string): ShortcutKeyResult {
} else if (lowercaseKey === "alt") {
return isMacOS() ? { symbol: "⌥", readable: "Option" } : { symbol: "Alt", readable: "Alt" }
} else if (lowercaseKey === "shift") {
- return { symbol: "⇧", readable: "Shift" }
- } else if (lowercaseKey === "control") {
- return { symbol: "⌃", readable: "Control" }
- } else if (lowercaseKey === "windows" && !isMacOS()) {
- return { symbol: "Win", readable: "Windows" }
+ return isMacOS() ? { symbol: "⇧", readable: "Shift" } : { symbol: "Shift", readable: "Shift" }
} else {
return { symbol: key.toUpperCase(), readable: key }
}
@@ -68,21 +64,3 @@ export function getShortcutKey(key: string): ShortcutKeyResult {
export function getShortcutKeys(keys: string[]): ShortcutKeyResult[] {
return keys.map(key => getShortcutKey(key))
}
-
-export function getSpecialShortcut(shortcutName: string): ShortcutKeyResult[] {
- if (shortcutName === "expandToolbar") {
- return isMacOS()
- ? [getShortcutKey("control"), getShortcutKey("mod"), getShortcutKey("n")]
- : [getShortcutKey("mod"), getShortcutKey("windows"), getShortcutKey("n")]
- }
-
- return []
-}
-
-export function formatShortcut(shortcutKeys: ShortcutKeyResult[]): string {
- return shortcutKeys.map(key => key.symbol).join("")
-}
-
-export function formatReadableShortcut(shortcutKeys: ShortcutKeyResult[]): string {
- return shortcutKeys.map(key => key.readable).join(" + ")
-}
diff --git a/web/store/keydown-manager.ts b/web/store/keydown-manager.ts
new file mode 100644
index 00000000..d6617dc1
--- /dev/null
+++ b/web/store/keydown-manager.ts
@@ -0,0 +1,3 @@
+import { atom } from "jotai"
+
+export const keyboardDisableSourcesAtom = atom>(new Set())
diff --git a/web/store/sidebar.ts b/web/store/sidebar.ts
index 79854872..a3638924 100644
--- a/web/store/sidebar.ts
+++ b/web/store/sidebar.ts
@@ -5,4 +5,3 @@ export const toggleCollapseAtom = atom(
get => get(isCollapseAtom),
(get, set) => set(isCollapseAtom, !get(isCollapseAtom))
)
-export const showHotkeyPanelAtom = atom(false)